diff --git a/core/server/web/api/README.md b/core/server/web/api/README.md new file mode 100644 index 0000000000..ca7e784a72 --- /dev/null +++ b/core/server/web/api/README.md @@ -0,0 +1,7 @@ +# Ghost APIs + +Ghost is moving towards providing more robust APIs in the future. A plan and decisions can be found [here](https://github.com/TryGhost/Ghost/issues/9866). + +## WARNING! + +The v2 API (`/ghost/api/v2/*` endpoints) is to be considered under active development until this message is removed. Please use with caution and don't rely too heavy on it just yet :) diff --git a/core/server/web/api/app.js b/core/server/web/api/v0.1/app.js similarity index 78% rename from core/server/web/api/app.js rename to core/server/web/api/v0.1/app.js index ee048b852b..e6633aae3a 100644 --- a/core/server/web/api/app.js +++ b/core/server/web/api/v0.1/app.js @@ -9,16 +9,16 @@ const debug = require('ghost-ignition').debug('api'), // Include the middleware // API specific - versionMatch = require('../middleware/api/version-match'), // global + versionMatch = require('../../middleware/api/version-match'), // global // Shared bodyParser = require('body-parser'), // global, shared - cacheControl = require('../middleware/cache-control'), // global, shared - maintenance = require('../middleware/maintenance'), // global, shared - errorHandler = require('../middleware/error-handler'); // global, shared + cacheControl = require('../../middleware/cache-control'), // global, shared + maintenance = require('../../middleware/maintenance'), // global, shared + errorHandler = require('../../middleware/error-handler'); // global, shared module.exports = function setupApiApp() { - debug('API setup start'); + debug('API v0.1 setup start'); const apiApp = express(); // @TODO finish refactoring this away. @@ -54,7 +54,7 @@ module.exports = function setupApiApp() { apiApp.use(errorHandler.resourceNotFound); apiApp.use(errorHandler.handleJSONResponse); - debug('API setup end'); + debug('API v0.1 setup end'); return apiApp; }; diff --git a/core/server/web/api/middleware.js b/core/server/web/api/v0.1/middleware.js similarity index 85% rename from core/server/web/api/middleware.js rename to core/server/web/api/v0.1/middleware.js index caa7fbc40d..cc54ede759 100644 --- a/core/server/web/api/middleware.js +++ b/core/server/web/api/v0.1/middleware.js @@ -1,7 +1,7 @@ -const prettyURLs = require('../middleware/pretty-urls'), - cors = require('../middleware/api/cors'), - urlRedirects = require('../middleware/url-redirects'), - auth = require('../../services/auth'); +const prettyURLs = require('../../middleware/pretty-urls'), + cors = require('../../middleware/api/cors'), + urlRedirects = require('../../middleware/url-redirects'), + auth = require('../../../services/auth'); /** * Auth Middleware Packages diff --git a/core/server/web/api/routes.js b/core/server/web/api/v0.1/routes.js similarity index 95% rename from core/server/web/api/routes.js rename to core/server/web/api/v0.1/routes.js index 45b4a3c99b..c4bfadc621 100644 --- a/core/server/web/api/routes.js +++ b/core/server/web/api/v0.1/routes.js @@ -1,26 +1,25 @@ const express = require('express'), // This essentially provides the controllers for the routes - api = require('../../api'), + api = require('../../../api'), // Middleware mw = require('./middleware'), // API specific - auth = require('../../services/auth'), - cors = require('../middleware/api/cors'), - brute = require('../middleware/brute'), + auth = require('../../../services/auth'), + cors = require('../../middleware/api/cors'), + brute = require('../../middleware/brute'), // Handling uploads & imports tmpdir = require('os').tmpdir, upload = require('multer')({dest: tmpdir()}), - validation = require('../middleware/validation'), - image = require('../middleware/image'), + validation = require('../../middleware/validation'), + image = require('../../middleware/image'), // Temporary // @TODO find a more appy way to do this! - labs = require('../middleware/labs'); + labs = require('../../middleware/labs'); -// @TODO refactor/clean this up - how do we want the routing to work long term? module.exports = function apiRoutes() { const apiRouter = express.Router(); diff --git a/core/server/web/api/v2/admin/app.js b/core/server/web/api/v2/admin/app.js new file mode 100644 index 0000000000..176c477e7e --- /dev/null +++ b/core/server/web/api/v2/admin/app.js @@ -0,0 +1,60 @@ +// # API routes +const debug = require('ghost-ignition').debug('api'), + boolParser = require('express-query-boolean'), + express = require('express'), + + // routes + routes = require('./routes'), + + // Include the middleware + + // API specific + versionMatch = require('../../../middleware/api/version-match'), // global + + // Shared + bodyParser = require('body-parser'), // global, shared + cacheControl = require('../../../middleware/cache-control'), // global, shared + maintenance = require('../../../middleware/maintenance'), // global, shared + errorHandler = require('../../../middleware/error-handler'); // global, shared + +module.exports = function setupApiApp() { + debug('Admin API v2 setup start'); + const apiApp = express(); + + // @TODO finish refactoring this away. + apiApp.use(function setIsAdmin(req, res, next) { + // api === isAdmin + res.isAdmin = true; + next(); + }); + + // API middleware + + // Body parsing + apiApp.use(bodyParser.json({limit: '1mb'})); + apiApp.use(bodyParser.urlencoded({extended: true, limit: '1mb'})); + + // Query parsing + apiApp.use(boolParser()); + + // send 503 json response in case of maintenance + apiApp.use(maintenance); + + // Check version matches for API requests, depends on res.locals.safeVersion being set + // Therefore must come after themeHandler.ghostLocals, for now + apiApp.use(versionMatch); + + // API shouldn't be cached + apiApp.use(cacheControl('private')); + + // Routing + apiApp.use(routes()); + + // API error handling + apiApp.use(errorHandler.resourceNotFound); + apiApp.use(errorHandler.handleJSONResponse); + + debug('Admin API v2 setup end'); + + return apiApp; +}; diff --git a/core/server/web/api/v2/admin/middleware.js b/core/server/web/api/v2/admin/middleware.js new file mode 100644 index 0000000000..16c644f649 --- /dev/null +++ b/core/server/web/api/v2/admin/middleware.js @@ -0,0 +1,30 @@ +const prettyURLs = require('../../../middleware/pretty-urls'), + cors = require('../../../middleware/api/cors'), + urlRedirects = require('../../../middleware/url-redirects'), + auth = require('../../../../services/auth'); + +/** + * Authentication for private endpoints + */ +module.exports.authenticatePrivate = [ + auth.authenticate.authenticateClient, + auth.authenticate.authenticateUser, + auth.authorize.requiresAuthorizedUser, + cors, + urlRedirects, + prettyURLs +]; + +/** + * Authentication for client endpoints + */ +module.exports.authenticateClient = function authenticateClient(client) { + return [ + auth.authenticate.authenticateClient, + auth.authenticate.authenticateUser, + auth.authorize.requiresAuthorizedClient(client), + cors, + urlRedirects, + prettyURLs + ]; +}; diff --git a/core/server/web/api/v2/admin/routes.js b/core/server/web/api/v2/admin/routes.js new file mode 100644 index 0000000000..3ae69808e4 --- /dev/null +++ b/core/server/web/api/v2/admin/routes.js @@ -0,0 +1,222 @@ +const express = require('express'), + // This essentially provides the controllers for the routes + api = require('../../../../api'), + + // Middleware + mw = require('./middleware'), + + // API specific + auth = require('../../../../services/auth'), + cors = require('../../../middleware/api/cors'), + brute = require('../../../middleware/brute'), + + // Handling uploads & imports + tmpdir = require('os').tmpdir, + upload = require('multer')({dest: tmpdir()}), + validation = require('../../../middleware/validation'), + image = require('../../../middleware/image'), + + // Temporary + // @TODO find a more appy way to do this! + labs = require('../../../middleware/labs'); + +module.exports = function apiRoutes() { + const router = express.Router(); + + // alias delete with del + router.del = router.delete; + + // ## CORS pre-flight check + router.options('*', cors); + + // ## Configuration + router.get('/configuration', api.http(api.configuration.read)); + router.get('/configuration/:key', mw.authenticatePrivate, api.http(api.configuration.read)); + + // ## Posts + router.get('/posts', mw.authenticatePrivate, api.http(api.posts.browse)); + + router.post('/posts', mw.authenticatePrivate, api.http(api.posts.add)); + router.get('/posts/:id', mw.authenticatePrivate, api.http(api.posts.read)); + router.get('/posts/slug/:slug', mw.authenticatePrivate, api.http(api.posts.read)); + router.put('/posts/:id', mw.authenticatePrivate, api.http(api.posts.edit)); + router.del('/posts/:id', mw.authenticatePrivate, api.http(api.posts.destroy)); + + // ## Schedules + router.put('/schedules/posts/:id', [ + auth.authenticate.authenticateClient, + auth.authenticate.authenticateUser + ], api.http(api.schedules.publishPost)); + + // ## Settings + router.get('/settings/routes/yaml', mw.authenticatePrivate, api.http(api.settings.download)); + router.post('/settings/routes/yaml', + mw.authenticatePrivate, + upload.single('routes'), + validation.upload({type: 'routes'}), + api.http(api.settings.upload) + ); + + router.get('/settings', mw.authenticatePrivate, api.http(api.settings.browse)); + router.get('/settings/:key', mw.authenticatePrivate, api.http(api.settings.read)); + router.put('/settings', mw.authenticatePrivate, api.http(api.settings.edit)); + + // ## Users + router.get('/users', mw.authenticatePrivate, api.http(api.users.browse)); + router.get('/users/:id', mw.authenticatePrivate, api.http(api.users.read)); + router.get('/users/slug/:slug', mw.authenticatePrivate, api.http(api.users.read)); + // NOTE: We don't expose any email addresses via the public api. + router.get('/users/email/:email', mw.authenticatePrivate, api.http(api.users.read)); + + router.put('/users/password', mw.authenticatePrivate, api.http(api.users.changePassword)); + router.put('/users/owner', mw.authenticatePrivate, api.http(api.users.transferOwnership)); + router.put('/users/:id', mw.authenticatePrivate, api.http(api.users.edit)); + router.del('/users/:id', mw.authenticatePrivate, api.http(api.users.destroy)); + + // ## Tags + router.get('/tags', mw.authenticatePrivate, api.http(api.tags.browse)); + router.get('/tags/:id', mw.authenticatePrivate, api.http(api.tags.read)); + router.get('/tags/slug/:slug', mw.authenticatePrivate, api.http(api.tags.read)); + router.post('/tags', mw.authenticatePrivate, api.http(api.tags.add)); + router.put('/tags/:id', mw.authenticatePrivate, api.http(api.tags.edit)); + router.del('/tags/:id', mw.authenticatePrivate, api.http(api.tags.destroy)); + + // ## Subscribers + router.get('/subscribers', labs.subscribers, mw.authenticatePrivate, api.http(api.subscribers.browse)); + router.get('/subscribers/csv', labs.subscribers, mw.authenticatePrivate, api.http(api.subscribers.exportCSV)); + router.post('/subscribers/csv', + labs.subscribers, + mw.authenticatePrivate, + upload.single('subscribersfile'), + validation.upload({type: 'subscribers'}), + api.http(api.subscribers.importCSV) + ); + router.get('/subscribers/:id', labs.subscribers, mw.authenticatePrivate, api.http(api.subscribers.read)); + router.get('/subscribers/email/:email', labs.subscribers, mw.authenticatePrivate, api.http(api.subscribers.read)); + router.post('/subscribers', labs.subscribers, mw.authenticatePrivate, api.http(api.subscribers.add)); + router.put('/subscribers/:id', labs.subscribers, mw.authenticatePrivate, api.http(api.subscribers.edit)); + router.del('/subscribers/:id', labs.subscribers, mw.authenticatePrivate, api.http(api.subscribers.destroy)); + router.del('/subscribers/email/:email', labs.subscribers, mw.authenticatePrivate, api.http(api.subscribers.destroy)); + + // ## Roles + router.get('/roles/', mw.authenticatePrivate, api.http(api.roles.browse)); + + // ## Clients + router.get('/clients/slug/:slug', api.http(api.clients.read)); + + // ## Slugs + router.get('/slugs/:type/:name', mw.authenticatePrivate, api.http(api.slugs.generate)); + + // ## Themes + router.get('/themes/', mw.authenticatePrivate, api.http(api.themes.browse)); + + router.get('/themes/:name/download', + mw.authenticatePrivate, + api.http(api.themes.download) + ); + + router.post('/themes/upload', + mw.authenticatePrivate, + upload.single('theme'), + validation.upload({type: 'themes'}), + api.http(api.themes.upload) + ); + + router.put('/themes/:name/activate', + mw.authenticatePrivate, + api.http(api.themes.activate) + ); + + router.del('/themes/:name', + mw.authenticatePrivate, + api.http(api.themes.destroy) + ); + + // ## Notifications + router.get('/notifications', mw.authenticatePrivate, api.http(api.notifications.browse)); + router.post('/notifications', mw.authenticatePrivate, api.http(api.notifications.add)); + router.del('/notifications/:id', mw.authenticatePrivate, api.http(api.notifications.destroy)); + + // ## DB + router.get('/db', mw.authenticatePrivate, api.http(api.db.exportContent)); + router.post('/db', + mw.authenticatePrivate, + upload.single('importfile'), + validation.upload({type: 'db'}), + api.http(api.db.importContent) + ); + router.del('/db', mw.authenticatePrivate, api.http(api.db.deleteAllContent)); + + // ## Mail + router.post('/mail', mw.authenticatePrivate, api.http(api.mail.send)); + router.post('/mail/test', mw.authenticatePrivate, api.http(api.mail.sendTest)); + + // ## Slack + router.post('/slack/test', mw.authenticatePrivate, api.http(api.slack.sendTest)); + + // ## Authentication + router.post('/authentication/passwordreset', + brute.globalReset, + brute.userReset, + api.http(api.authentication.generateResetToken) + ); + router.put('/authentication/passwordreset', brute.globalBlock, api.http(api.authentication.resetPassword)); + router.post('/authentication/invitation', api.http(api.authentication.acceptInvitation)); + router.get('/authentication/invitation', api.http(api.authentication.isInvitation)); + router.post('/authentication/setup', api.http(api.authentication.setup)); + router.put('/authentication/setup', mw.authenticatePrivate, api.http(api.authentication.updateSetup)); + router.get('/authentication/setup', api.http(api.authentication.isSetup)); + + router.post('/authentication/token', + mw.authenticateClient(), + brute.globalBlock, + brute.userLogin, + auth.oauth.generateAccessToken + ); + + router.post('/authentication/revoke', mw.authenticatePrivate, api.http(api.authentication.revoke)); + + // ## Uploads + // @TODO: rename endpoint to /images/upload (or similar) + router.post('/uploads', + mw.authenticatePrivate, + upload.single('uploadimage'), + validation.upload({type: 'images'}), + image.normalize, + api.http(api.uploads.add) + ); + + router.post('/db/backup', mw.authenticateClient('Ghost Backup'), api.http(api.db.backupContent)); + + router.post('/uploads/icon', + mw.authenticatePrivate, + upload.single('uploadimage'), + validation.upload({type: 'icons'}), + validation.blogIcon(), + api.http(api.uploads.add) + ); + + // ## Invites + router.get('/invites', mw.authenticatePrivate, api.http(api.invites.browse)); + router.get('/invites/:id', mw.authenticatePrivate, api.http(api.invites.read)); + router.post('/invites', mw.authenticatePrivate, api.http(api.invites.add)); + router.del('/invites/:id', mw.authenticatePrivate, api.http(api.invites.destroy)); + + // ## Redirects (JSON based) + router.get('/redirects/json', mw.authenticatePrivate, api.http(api.redirects.download)); + router.post('/redirects/json', + mw.authenticatePrivate, + upload.single('redirects'), + validation.upload({type: 'redirects'}), + api.http(api.redirects.upload) + ); + + // ## Webhooks (RESTHooks) + router.post('/webhooks', mw.authenticatePrivate, api.http(api.webhooks.add)); + router.del('/webhooks/:id', mw.authenticatePrivate, api.http(api.webhooks.destroy)); + + // ## Oembed (fetch response from oembed provider) + router.get('/oembed', mw.authenticatePrivate, api.http(api.oembed.read)); + + return router; +}; diff --git a/core/server/web/api/v2/content/app.js b/core/server/web/api/v2/content/app.js new file mode 100644 index 0000000000..0408f24986 --- /dev/null +++ b/core/server/web/api/v2/content/app.js @@ -0,0 +1,48 @@ +// # API routes +const debug = require('ghost-ignition').debug('api'), + boolParser = require('express-query-boolean'), + express = require('express'), + + // routes + routes = require('./routes'), + + // Include the middleware + + // Shared + cacheControl = require('../../../middleware/cache-control'), // global, shared + maintenance = require('../../../middleware/maintenance'), // global, shared + errorHandler = require('../../../middleware/error-handler'); // global, shared + +module.exports = function setupApiApp() { + debug('Content API v2 setup start'); + const apiApp = express(); + + // @TODO finish refactoring this away. + apiApp.use(function setIsAdmin(req, res, next) { + // api === isAdmin + res.isAdmin = true; + next(); + }); + + // API middleware + + // Query parsing + apiApp.use(boolParser()); + + // send 503 json response in case of maintenance + apiApp.use(maintenance); + + // API shouldn't be cached + apiApp.use(cacheControl('private')); + + // Routing + apiApp.use(routes()); + + // API error handling + apiApp.use(errorHandler.resourceNotFound); + apiApp.use(errorHandler.handleJSONResponse); + + debug('Content API v2 setup end'); + + return apiApp; +}; diff --git a/core/server/web/api/v2/content/middleware.js b/core/server/web/api/v2/content/middleware.js new file mode 100644 index 0000000000..4bcd15c35e --- /dev/null +++ b/core/server/web/api/v2/content/middleware.js @@ -0,0 +1,26 @@ +const prettyURLs = require('../../../middleware/pretty-urls'), + cors = require('../../../middleware/api/cors'), + urlRedirects = require('../../../middleware/url-redirects'), + auth = require('../../../../services/auth'); + +/** + * Auth Middleware Packages + * + * IMPORTANT + * - cors middleware MUST happen before pretty urls, because otherwise cors header can get lost on redirect + * - cors middleware MUST happen after authenticateClient, because authenticateClient reads the trusted domains + * - url redirects MUST happen after cors, otherwise cors header can get lost on redirect + */ + +/** + * Authentication for public endpoints + */ +module.exports.authenticatePublic = [ + auth.authenticate.authenticateClient, + auth.authenticate.authenticateUser, + // This is a labs-enabled middleware + auth.authorize.requiresAuthorizedUserPublicAPI, + cors, + urlRedirects, + prettyURLs +]; diff --git a/core/server/web/api/v2/content/routes.js b/core/server/web/api/v2/content/routes.js new file mode 100644 index 0000000000..4537daef9a --- /dev/null +++ b/core/server/web/api/v2/content/routes.js @@ -0,0 +1,49 @@ +const express = require('express'), + // This essentially provides the controllers for the routes + api = require('../../../../api'), + + // Middleware + mw = require('./middleware'), + + // API specific + cors = require('../../../middleware/api/cors'), + + // Temporary + // @TODO find a more appy way to do this! + labs = require('../../../middleware/labs'); + +module.exports = function apiRoutes() { + const router = express.Router(); + + // alias delete with del + router.del = router.delete; + + // ## CORS pre-flight check + router.options('*', cors); + + // ## Configuration + router.get('/configuration', api.http(api.configuration.read)); + + // ## Posts + router.get('/posts', mw.authenticatePublic, api.http(api.posts.browse)); + router.get('/posts/:id', mw.authenticatePublic, api.http(api.posts.read)); + router.get('/posts/slug/:slug', mw.authenticatePublic, api.http(api.posts.read)); + + // ## Users + router.get('/users', mw.authenticatePublic, api.http(api.users.browse)); + router.get('/users/:id', mw.authenticatePublic, api.http(api.users.read)); + router.get('/users/slug/:slug', mw.authenticatePublic, api.http(api.users.read)); + + // ## Tags + router.get('/tags', mw.authenticatePublic, api.http(api.tags.browse)); + router.get('/tags/:id', mw.authenticatePublic, api.http(api.tags.read)); + router.get('/tags/slug/:slug', mw.authenticatePublic, api.http(api.tags.read)); + + // ## Subscribers + router.post('/subscribers', labs.subscribers, mw.authenticatePublic, api.http(api.subscribers.add)); + + // ## Clients + router.get('/clients/slug/:slug', api.http(api.clients.read)); + + return router; +}; diff --git a/core/server/web/parent-app.js b/core/server/web/parent-app.js index 5cd546de6a..ec292e5d5f 100644 --- a/core/server/web/parent-app.js +++ b/core/server/web/parent-app.js @@ -45,7 +45,9 @@ module.exports = function setupParentApp(options = {}) { // API // @TODO: finish refactoring the API app // @TODO: decide what to do with these paths - config defaults? config overrides? - parentApp.use('/ghost/api/v0.1/', require('./api/app')()); + parentApp.use('/ghost/api/v0.1/', require('./api/v0.1/app')()); + parentApp.use('/ghost/api/v2/content/', require('./api/v2/content/app')()); + parentApp.use('/ghost/api/v2/admin/', require('./api/v2/admin/app')()); // ADMIN parentApp.use('/ghost', require('./admin')()); diff --git a/core/test/unit/web/parent-app_spec.js b/core/test/unit/web/parent-app_spec.js new file mode 100644 index 0000000000..658b77923a --- /dev/null +++ b/core/test/unit/web/parent-app_spec.js @@ -0,0 +1,57 @@ +var should = require('should'), + sinon = require('sinon'), + proxyquire = require('proxyquire'), + sandbox = sinon.sandbox.create(); + +describe('parent app', function () { + let expressStub; + let use; + let apiV01Spy; + let apiContentV2Spy; + let apiAdminV2Spy; + let parentApp; + let adminSpy; + let siteSpy; + + beforeEach(function () { + use = sandbox.spy(); + expressStub = () => ({ + use, + enable: () => {} + }); + + apiV01Spy = sinon.spy(); + apiContentV2Spy = sinon.spy(); + apiAdminV2Spy = sinon.spy(); + adminSpy = sinon.spy(); + siteSpy = sinon.spy(); + + parentApp = proxyquire('../../../server/web/parent-app', { + express: expressStub, + './api/v0.1/app': apiV01Spy, + './api/v2/content/app': apiContentV2Spy, + './api/v2/admin/app': apiAdminV2Spy, + './admin': adminSpy, + './site': siteSpy + }); + }); + + afterEach(function () { + sandbox.restore(); + }); + + it('should mount 5 apps and assign correct routes to them', function () { + parentApp(); + + use.calledWith('/ghost/api/v0.1/').should.be.true(); + use.calledWith('/ghost/api/v2/content/').should.be.true(); + use.calledWith('/ghost/api/v2/admin/').should.be.true(); + use.calledWith('/ghost').should.be.true(); + + apiV01Spy.called.should.be.true(); + apiContentV2Spy.called.should.be.true(); + apiAdminV2Spy.called.should.be.true(); + adminSpy.called.should.be.true(); + siteSpy.called.should.be.true(); + }); +}); diff --git a/package.json b/package.json index 3934c9b25f..56c7e77302 100644 --- a/package.json +++ b/package.json @@ -128,6 +128,7 @@ "minimist": "1.2.0", "mocha": "4.1.0", "nock": "9.4.0", + "proxyquire": "2.1.0", "rewire": "3.0.2", "should": "13.2.1", "should-http": "0.1.1", diff --git a/yarn.lock b/yarn.lock index de34197417..a29a1d7e93 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1934,6 +1934,13 @@ file-sync-cmp@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/file-sync-cmp/-/file-sync-cmp-0.1.1.tgz#a5e7a8ffbfa493b43b923bbd4ca89a53b63b612b" +fill-keys@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/fill-keys/-/fill-keys-1.0.2.tgz#9a8fa36f4e8ad634e3bf6b4f3c8882551452eb20" + dependencies: + is-object "~1.0.1" + merge-descriptors "~1.0.0" + fill-range@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" @@ -3065,7 +3072,7 @@ is-number@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff" -is-object@^1.0.1: +is-object@^1.0.1, is-object@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.1.tgz#8952688c5ec2ffd6b03ecc85e769e02903083470" @@ -3782,7 +3789,7 @@ meow@^3.1.0, meow@^3.3.0: redent "^1.0.0" trim-newlines "^1.0.0" -merge-descriptors@1.0.1: +merge-descriptors@1.0.1, merge-descriptors@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" @@ -3971,6 +3978,10 @@ mocha@^3.1.2: mkdirp "0.5.1" supports-color "3.1.2" +module-not-found-error@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/module-not-found-error/-/module-not-found-error-1.0.1.tgz#cf8b4ff4f29640674d6cdd02b0e3bc523c2bbdc0" + moment-timezone@0.5.21: version "0.5.21" resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.21.tgz#3cba247d84492174dbf71de2a9848fa13207b845" @@ -4944,6 +4955,14 @@ proxy-addr@~2.0.3: forwarded "~0.1.2" ipaddr.js "1.6.0" +proxyquire@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/proxyquire/-/proxyquire-2.1.0.tgz#c2263a38bf0725f2ae950facc130e27510edce8d" + dependencies: + fill-keys "^1.0.2" + module-not-found-error "^1.0.0" + resolve "~1.8.1" + pseudomap@^1.0.1, pseudomap@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" @@ -5235,6 +5254,12 @@ resolve@1.7.1, resolve@^1.1.6, resolve@^1.1.7, resolve@^1.4.0: dependencies: path-parse "^1.0.5" +resolve@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.8.1.tgz#82f1ec19a423ac1fbd080b0bab06ba36e84a7a26" + dependencies: + path-parse "^1.0.5" + restore-cursor@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"