0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-10 23:36:14 -05:00

Replace error handler middleware with @tryghost/mw-error-handler (#13879)

refs: https://github.com/TryGhost/Toolbox/issues/137

Extract error handling middleware and replace with a package.
This commit is contained in:
Sam Lord 2021-12-14 15:18:46 +00:00 committed by GitHub
parent 271d2c02b0
commit 97c68dd388
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 84 additions and 285 deletions

View file

@ -7,7 +7,7 @@ const config = require('../../../shared/config');
const helpers = require('../../services/routing/helpers');
// @TODO: make this properly shared code
const shared = require('../../../server/web/shared/middleware/error-handler');
const {prepareError} = require('@tryghost/mw-error-handler');
const messages = {
oopsErrorTemplateHasError: 'Oops, seems there is an error in the error template.',
@ -85,7 +85,7 @@ const themeErrorRenderer = (err, req, res, next) => {
module.exports.handleThemeResponse = [
// Make sure the error can be served
shared.prepareError,
prepareError,
// Handle the error in Sentry
sentry.errorHandler,
// Render the error using theme template

View file

@ -20,6 +20,7 @@ const offersService = require('../../server/services/offers');
const customRedirects = require('../../server/services/redirects');
const siteRoutes = require('./routes');
const shared = require('../../server/web/shared');
const errorHandler = require('@tryghost/mw-error-handler');
const mw = require('./middleware');
const labs = require('../../shared/labs');
@ -176,7 +177,7 @@ module.exports = function setupSiteApp(options = {}) {
siteApp.use(SiteRouter);
// ### Error handlers
siteApp.use(shared.middleware.errorHandler.pageNotFound);
siteApp.use(errorHandler.pageNotFound);
config.get('apps:internal').forEach((appName) => {
const app = require(path.join(config.get('paths').internalAppPath, appName));

View file

@ -16,14 +16,11 @@ class MembersConfigProvider {
* @param {{get: (key: string) => any}} options.settingsCache
* @param {{get: (key: string) => any}} options.config
* @param {any} options.urlUtils
* @param {any} options.logging
* @param {{original: string}} options.ghostVersion
*/
constructor(options) {
this._settingsCache = options.settingsCache;
this._config = options.config;
this._urlUtils = options.urlUtils;
this._ghostVersion = options.ghostVersion;
}
/**

View file

@ -13,7 +13,6 @@ const labsService = require('../../../shared/labs');
const settingsCache = require('../../../shared/settings-cache');
const config = require('../../../shared/config');
const models = require('../../models');
const ghostVersion = require('@tryghost/version');
const _ = require('lodash');
const {GhostMailer} = require('../mail');
const jobsService = require('../jobs');
@ -35,8 +34,7 @@ const ghostMailer = new GhostMailer();
const membersConfig = new MembersConfigProvider({
config,
settingsCache,
urlUtils,
ghostVersion
urlUtils
});
let membersApi;

View file

@ -1,10 +1,8 @@
const settingsCache = require('../../../shared/settings-cache');
const ghostVersion = require('@tryghost/version');
const Notifications = require('./notifications');
const models = require('../../models');
module.exports.notifications = new Notifications({
settingsCache,
ghostVersion: ghostVersion.full,
SettingsModel: models.Settings
});

View file

@ -3,6 +3,7 @@ const semver = require('semver');
const Promise = require('bluebird');
const _ = require('lodash');
const errors = require('@tryghost/errors');
const ghostVersion = require('@tryghost/version');
const tpl = require('@tryghost/tpl');
const ObjectId = require('bson-objectid');
@ -16,12 +17,10 @@ class Notifications {
*
* @param {Object} options
* @param {Object} options.settingsCache - settings cache instance
* @param {String} options.ghostVersion - Ghost instance version in "full" format - major.minor.patch
* @param {Object} options.SettingsModel - Ghost's Setting model instance
*/
constructor({settingsCache, ghostVersion, SettingsModel}) {
constructor({settingsCache, SettingsModel}) {
this.settingsCache = settingsCache;
this.ghostVersion = ghostVersion;
this.SettingsModel = SettingsModel;
}
@ -74,7 +73,7 @@ class Notifications {
browse({user}) {
let allNotifications = this.fetchAllNotifications();
allNotifications = _.orderBy(allNotifications, 'addedAt', 'desc');
const blogVersion = this.ghostVersion.match(/^(\d+\.)(\d+\.)(\d+)/);
const blogVersion = ghostVersion.full.match(/^(\d+\.)(\d+\.)(\d+)/);
allNotifications = allNotifications.filter((notification) => {
if (notification.createdAtVersion && !this.wasSeen(notification, user)) {
@ -128,7 +127,7 @@ class Notifications {
location: 'bottom',
status: 'alert',
id: ObjectId().toHexString(),
createdAtVersion: this.ghostVersion
createdAtVersion: ghostVersion.full
};
const overrides = {

View file

@ -5,6 +5,8 @@ const config = require('../../../shared/config');
const constants = require('@tryghost/constants');
const urlUtils = require('../../../shared/url-utils');
const shared = require('../shared');
const errorHandler = require('@tryghost/mw-error-handler');
const sentry = require('../../../shared/sentry');
const redirectAdminUrls = require('./middleware/redirect-admin-urls');
module.exports = function setupAdminApp() {
@ -44,8 +46,8 @@ module.exports = function setupAdminApp() {
// Finally, routing
adminApp.get('*', require('./controller'));
adminApp.use(shared.middleware.errorHandler.pageNotFound);
adminApp.use(shared.middleware.errorHandler.handleHTMLResponse);
adminApp.use(errorHandler.pageNotFound);
adminApp.use(errorHandler.handleHTMLResponse(sentry));
debug('Admin setup end');

View file

@ -2,7 +2,8 @@ const debug = require('@tryghost/debug')('web:api:default:app');
const config = require('../../../shared/config');
const express = require('../../../shared/express');
const urlUtils = require('../../../shared/url-utils');
const errorHandler = require('../shared/middleware/error-handler');
const sentry = require('../../../shared/sentry');
const errorHandler = require('@tryghost/mw-error-handler');
module.exports = function setupApiApp() {
debug('Parent API setup start');
@ -26,7 +27,7 @@ module.exports = function setupApiApp() {
// Error handling for requests to non-existent API versions
apiApp.use(errorHandler.resourceNotFound);
apiApp.use(errorHandler.handleJSONResponse);
apiApp.use(errorHandler.handleJSONResponse(sentry));
debug('Parent API setup end');
return apiApp;

View file

@ -4,6 +4,8 @@ const express = require('../../../../../shared/express');
const bodyParser = require('body-parser');
const shared = require('../../../shared');
const apiMw = require('../../middleware');
const errorHandler = require('@tryghost/mw-error-handler');
const sentry = require('../../../../../shared/sentry');
const routes = require('./routes');
module.exports = function setupApiApp() {
@ -30,8 +32,8 @@ module.exports = function setupApiApp() {
apiApp.use(routes());
// API error handling
apiApp.use(shared.middleware.errorHandler.resourceNotFound);
apiApp.use(shared.middleware.errorHandler.handleJSONResponseV2);
apiApp.use(errorHandler.resourceNotFound);
apiApp.use(errorHandler.handleJSONResponseV2(sentry));
debug('Admin API canary setup end');

View file

@ -2,8 +2,10 @@ const debug = require('@tryghost/debug')('web:api:canary:content:app');
const boolParser = require('express-query-boolean');
const bodyParser = require('body-parser');
const express = require('../../../../../shared/express');
const sentry = require('../../../../../shared/sentry');
const shared = require('../../../shared');
const routes = require('./routes');
const errorHandler = require('@tryghost/mw-error-handler');
module.exports = function setupApiApp() {
debug('Content API canary setup start');
@ -24,8 +26,8 @@ module.exports = function setupApiApp() {
apiApp.use(routes());
// API error handling
apiApp.use(shared.middleware.errorHandler.resourceNotFound);
apiApp.use(shared.middleware.errorHandler.handleJSONResponse);
apiApp.use(errorHandler.resourceNotFound);
apiApp.use(errorHandler.handleJSONResponse(sentry));
debug('Content API canary setup end');

View file

@ -1,10 +1,12 @@
const debug = require('@tryghost/debug')('web:v2:admin:app');
const boolParser = require('express-query-boolean');
const express = require('../../../../../shared/express');
const sentry = require('../../../../../shared/sentry');
const bodyParser = require('body-parser');
const shared = require('../../../shared');
const apiMw = require('../../middleware');
const routes = require('./routes');
const errorHandler = require('@tryghost/mw-error-handler');
module.exports = function setupApiApp() {
debug('Admin API v2 setup start');
@ -30,8 +32,8 @@ module.exports = function setupApiApp() {
apiApp.use(routes());
// API error handling
apiApp.use(shared.middleware.errorHandler.resourceNotFound);
apiApp.use(shared.middleware.errorHandler.handleJSONResponseV2);
apiApp.use(errorHandler.resourceNotFound);
apiApp.use(errorHandler.handleJSONResponseV2(sentry));
debug('Admin API v2 setup end');

View file

@ -2,8 +2,10 @@ const debug = require('@tryghost/debug')('web:api:v2:content:app');
const boolParser = require('express-query-boolean');
const bodyParser = require('body-parser');
const express = require('../../../../../shared/express');
const sentry = require('../../../../../shared/sentry');
const shared = require('../../../shared');
const routes = require('./routes');
const errorHandler = require('@tryghost/mw-error-handler');
module.exports = function setupApiApp() {
debug('Content API v2 setup start');
@ -24,8 +26,8 @@ module.exports = function setupApiApp() {
apiApp.use(routes());
// API error handling
apiApp.use(shared.middleware.errorHandler.resourceNotFound);
apiApp.use(shared.middleware.errorHandler.handleJSONResponse);
apiApp.use(errorHandler.resourceNotFound);
apiApp.use(errorHandler.handleJSONResponse(sentry));
debug('Content API v2 setup end');

View file

@ -1,10 +1,12 @@
const debug = require('@tryghost/debug')('web:v3:admin:app');
const boolParser = require('express-query-boolean');
const express = require('../../../../../shared/express');
const sentry = require('../../../../../shared/sentry');
const bodyParser = require('body-parser');
const shared = require('../../../shared');
const apiMw = require('../../middleware');
const routes = require('./routes');
const errorHandler = require('@tryghost/mw-error-handler');
module.exports = function setupApiApp() {
debug('Admin API v3 setup start');
@ -30,8 +32,8 @@ module.exports = function setupApiApp() {
apiApp.use(routes());
// API error handling
apiApp.use(shared.middleware.errorHandler.resourceNotFound);
apiApp.use(shared.middleware.errorHandler.handleJSONResponseV2);
apiApp.use(errorHandler.resourceNotFound);
apiApp.use(errorHandler.handleJSONResponseV2(sentry));
debug('Admin API v3 setup end');

View file

@ -2,8 +2,10 @@ const debug = require('@tryghost/debug')('web:api:v3:content:app');
const boolParser = require('express-query-boolean');
const bodyParser = require('body-parser');
const express = require('../../../../../shared/express');
const sentry = require('../../../../../shared/sentry');
const shared = require('../../../shared');
const routes = require('./routes');
const errorHandler = require('@tryghost/mw-error-handler');
module.exports = function setupApiApp() {
debug('Content API v3 setup start');
@ -24,8 +26,8 @@ module.exports = function setupApiApp() {
apiApp.use(routes());
// API error handling
apiApp.use(shared.middleware.errorHandler.resourceNotFound);
apiApp.use(shared.middleware.errorHandler.handleJSONResponse);
apiApp.use(errorHandler.resourceNotFound);
apiApp.use(errorHandler.handleJSONResponse(sentry));
debug('Content API v3 setup end');

View file

@ -4,10 +4,12 @@ const cors = require('cors');
const bodyParser = require('body-parser');
const express = require('../../../shared/express');
const urlUtils = require('../../../shared/url-utils');
const sentry = require('../../../shared/sentry');
const membersService = require('../../services/members');
const middleware = membersService.middleware;
const shared = require('../shared');
const labs = require('../../../shared/labs');
const errorHandler = require('@tryghost/mw-error-handler');
module.exports = function setupMembersApp() {
debug('Members App setup start');
@ -46,12 +48,12 @@ module.exports = function setupMembersApp() {
membersApp.post('/api/events', labs.enabledMiddleware('membersActivity'), middleware.loadMemberSession, (req, res, next) => membersService.api.middleware.createEvents(req, res, next));
// API error handling
membersApp.use('/api', shared.middleware.errorHandler.resourceNotFound);
membersApp.use('/api', shared.middleware.errorHandler.handleJSONResponseV2);
membersApp.use('/api', errorHandler.resourceNotFound);
membersApp.use('/api', errorHandler.handleJSONResponseV2(sentry));
// Webhook error handling
membersApp.use('/webhooks', shared.middleware.errorHandler.resourceNotFound);
membersApp.use('/webhooks', shared.middleware.errorHandler.handleJSONResponseV2);
membersApp.use('/webhooks', errorHandler.resourceNotFound);
membersApp.use('/webhooks', errorHandler.handleJSONResponseV2(sentry));
debug('Members App setup end');

View file

@ -1,224 +0,0 @@
const _ = require('lodash');
const debug = require('@tryghost/debug')('error-handler');
const errors = require('@tryghost/errors');
const tpl = require('@tryghost/tpl');
const sentry = require('../../../../shared/sentry');
const messages = {
pageNotFound: 'Page not found',
resourceNotFound: 'Resource not found',
actions: {
images: {
upload: 'upload image'
}
},
userMessages: {
BookshelfRelationsError: 'Database error, cannot {action}.',
InternalServerError: 'Internal server error, cannot {action}.',
IncorrectUsageError: 'Incorrect usage error, cannot {action}.',
NotFoundError: 'Resource not found error, cannot {action}.',
BadRequestError: 'Request not understood error, cannot {action}.',
UnauthorizedError: 'Authorisation error, cannot {action}.',
NoPermissionError: 'Permission error, cannot {action}.',
ValidationError: 'Validation error, cannot {action}.',
UnsupportedMediaTypeError: 'Unsupported media error, cannot {action}.',
TooManyRequestsError: 'Too many requests error, cannot {action}.',
MaintenanceError: 'Server down for maintenance, cannot {action}.',
MethodNotAllowedError: 'Method not allowed, cannot {action}.',
RequestEntityTooLargeError: 'Request too large, cannot {action}.',
TokenRevocationError: 'Token is not available, cannot {action}.',
VersionMismatchError: 'Version mismatch error, cannot {action}.',
DataExportError: 'Error exporting content.',
DataImportError: 'Duplicated entry, cannot save {action}.',
DatabaseVersionError: 'Database version compatibility error, cannot {action}.',
EmailError: 'Error sending email!',
ThemeValidationError: 'Theme validation error, cannot {action}.',
HostLimitError: 'Host Limit error, cannot {action}.',
DisabledFeatureError: 'Theme validation error, the {{{helperName}}} helper is not available. Cannot {action}.',
UpdateCollisionError: 'Saving failed! Someone else is editing this post.'
}
};
const updateStack = (err) => {
let stackbits = err.stack.split(/\n/g);
// We build this up backwards, so we always insert at position 1
if (process.env.NODE_ENV === 'production' || err.statusCode === 404) {
// In production mode, remove the stack trace
stackbits.splice(1, stackbits.length - 1);
} else {
// In dev mode, clearly mark the strack trace
stackbits.splice(1, 0, `Stack Trace:`);
}
// Add in our custom cotext and help methods
if (err.help) {
stackbits.splice(1, 0, `${err.help}`);
}
if (err.context) {
stackbits.splice(1, 0, `${err.context}`);
}
return stackbits.join('\n');
};
/**
* Get an error ready to be shown the the user
*/
module.exports.prepareError = (err, req, res, next) => {
debug(err);
if (Array.isArray(err)) {
err = err[0];
}
if (!errors.utils.isGhostError(err)) {
// We need a special case for 404 errors
if (err.statusCode && err.statusCode === 404) {
err = new errors.NotFoundError({
err: err
});
} else if (err.stack.match(/node_modules\/handlebars\//)) {
// Temporary handling of theme errors from handlebars
// @TODO remove this when #10496 is solved properly
err = new errors.IncorrectUsageError({
err: err,
message: err.message,
statusCode: err.statusCode
});
} else {
err = new errors.InternalServerError({
err: err,
message: err.message,
statusCode: err.statusCode
});
}
}
// used for express logging middleware see core/server/app.js
req.err = err;
// alternative for res.status();
res.statusCode = err.statusCode;
err.stack = updateStack(err);
// never cache errors
res.set({
'Cache-Control': 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0'
});
next(err);
};
const jsonErrorRenderer = (err, req, res, next) => { // eslint-disable-line no-unused-vars
res.json({
errors: [{
message: err.message,
context: err.context,
help: err.help,
errorType: err.errorType,
errorDetails: err.errorDetails,
ghostErrorCode: err.ghostErrorCode
}]
});
};
const jsonErrorRendererV2 = (err, req, res, next) => { // eslint-disable-line no-unused-vars
const userError = prepareUserMessage(err, req);
res.json({
errors: [{
message: userError.message || null,
context: userError.context || null,
type: err.errorType || null,
details: err.errorDetails || null,
property: err.property || null,
help: err.help || null,
code: err.code || null,
id: err.id || null
}]
});
};
const prepareUserMessage = (err, res) => {
const userError = {
message: err.message,
context: err.context
};
const docName = _.get(res, 'frameOptions.docName');
const method = _.get(res, 'frameOptions.method');
if (docName && method) {
let action;
const actionMap = {
browse: 'list',
read: 'read',
add: 'save',
edit: 'edit',
destroy: 'delete'
};
if (_.get(messages.actions, [docName, method])) {
action = tpl(messages.actions[docName][method]);
} else if (Object.keys(actionMap).includes(method)) {
let resource = docName;
if (method !== 'browse') {
resource = resource.replace(/s$/, '');
}
action = `${actionMap[method]} ${resource}`;
}
if (action) {
if (err.context) {
userError.context = `${err.message} ${err.context}`;
} else {
userError.context = err.message;
}
userError.message = tpl(messages.userMessages[err.name], {action: action});
}
}
return userError;
};
module.exports.resourceNotFound = (req, res, next) => {
next(new errors.NotFoundError({message: tpl(messages.resourceNotFound)}));
};
module.exports.pageNotFound = (req, res, next) => {
next(new errors.NotFoundError({message: tpl(messages.pageNotFound)}));
};
module.exports.handleJSONResponse = [
// Make sure the error can be served
module.exports.prepareError,
// Handle the error in Sentry
sentry.errorHandler,
// Render the error using JSON format
jsonErrorRenderer
];
module.exports.handleJSONResponseV2 = [
// Make sure the error can be served
module.exports.prepareError,
// Handle the error in Sentry
sentry.errorHandler,
// Render the error using JSON format
jsonErrorRendererV2
];
module.exports.handleHTMLResponse = [
// Make sure the error can be served
module.exports.prepareError,
// Handle the error in Sentry
sentry.errorHandler
];

View file

@ -11,10 +11,6 @@ module.exports = {
return require('./cache-control');
},
get errorHandler() {
return require('./error-handler');
},
get prettyUrls() {
return require('./pretty-urls');
},

View file

@ -66,7 +66,7 @@
"@tryghost/debug": "0.1.9",
"@tryghost/email-analytics-provider-mailgun": "1.0.7",
"@tryghost/email-analytics-service": "1.0.5",
"@tryghost/errors": "1.1.1",
"@tryghost/errors": "1.2.0",
"@tryghost/express-dynamic-redirects": "0.2.2",
"@tryghost/helpers": "1.1.54",
"@tryghost/image-transform": "1.0.25",
@ -86,6 +86,7 @@
"@tryghost/members-ssr": "1.0.16",
"@tryghost/metrics": "1.0.1",
"@tryghost/minifier": "0.1.8",
"@tryghost/mw-error-handler": "0.1.1",
"@tryghost/mw-session-from-token": "0.1.26",
"@tryghost/nodemailer": "0.3.8",
"@tryghost/package-json": "1.0.13",

View file

@ -1,6 +1,7 @@
const should = require('should');
const sinon = require('sinon');
const ghostVersion = require('@tryghost/version');
const moment = require('moment');
const Notifications = require('../../../../../core/server/services/notifications/notifications');
const {owner} = require('../../../../utils/fixtures/context');
@ -13,9 +14,9 @@ describe('Notifications Service', function () {
get: sinon.fake.returns(existingNotifications)
};
sinon.stub(ghostVersion, 'full').value('4.1.0');
const notificationsSvc = new Notifications({
settingsCache,
ghostVersion: '4.1.0'
settingsCache
});
const {allNotifications, notificationsToAdd} = notificationsSvc.add({
@ -60,9 +61,9 @@ describe('Notifications Service', function () {
}])
};
sinon.stub(ghostVersion, 'full').value('4.1.0');
const notificationSvc = new Notifications({
settingsCache,
ghostVersion: '4.1.0'
settingsCache
});
const notifications = notificationSvc.browse({user: owner});
@ -89,9 +90,9 @@ describe('Notifications Service', function () {
}])
};
sinon.stub(ghostVersion, 'full').value('4.0.0');
const notificationSvc = new Notifications({
settingsCache,
ghostVersion: '4.0.0'
settingsCache
});
const notifications = notificationSvc.browse({user: owner});
@ -118,9 +119,9 @@ describe('Notifications Service', function () {
}])
};
sinon.stub(ghostVersion, 'full').value('3.0.0');
const notificationSvc = new Notifications({
settingsCache,
ghostVersion: '3.0.0'
settingsCache
});
const notifications = notificationSvc.browse({user: owner});
@ -147,9 +148,9 @@ describe('Notifications Service', function () {
}])
};
sinon.stub(ghostVersion, 'full').value('4.0.0');
const notificationSvc = new Notifications({
settingsCache,
ghostVersion: '4.0.0'
settingsCache
});
const notifications = notificationSvc.browse({user: owner});
@ -176,9 +177,9 @@ describe('Notifications Service', function () {
}])
};
sinon.stub(ghostVersion, 'full').value('5.0.0');
const notificationSvc = new Notifications({
settingsCache,
ghostVersion: '5.0.0'
settingsCache
});
const notifications = notificationSvc.browse({user: owner});
@ -218,9 +219,9 @@ describe('Notifications Service', function () {
}])
};
sinon.stub(ghostVersion, 'full').value('4.1.0');
const notificationSvc = new Notifications({
settingsCache,
ghostVersion: '4.1.0'
settingsCache
});
const notifications = notificationSvc.browse({user: owner});
@ -243,7 +244,6 @@ describe('Notifications Service', function () {
const notificationSvc = new Notifications({
settingsCache,
ghostVersion: '5.0.0',
SettingsModel: {
edit: settingsModelStub
}
@ -269,9 +269,9 @@ describe('Notifications Service', function () {
};
const settingsModelStub = sinon.stub().resolves();
sinon.stub(ghostVersion, 'full').value('5.0.0');
const notificationSvc = new Notifications({
settingsCache,
ghostVersion: '5.0.0',
SettingsModel: {
edit: settingsModelStub
}

View file

@ -1412,10 +1412,10 @@
"@tryghost/debug" "^0.1.9"
lodash "^4.17.20"
"@tryghost/errors@1.1.1", "@tryghost/errors@^1.0.0", "@tryghost/errors@^1.1.0", "@tryghost/errors@^1.1.1":
version "1.1.1"
resolved "https://registry.npmjs.org/@tryghost/errors/-/errors-1.1.1.tgz"
integrity sha512-na0qB5sdy1BWgquzn+m530ohJ3fTeF451xUTR7I8b76TBEL9snnIkXCv5Qdjmnevmgod7aAGsHi2syyKFlvEvQ==
"@tryghost/errors@1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@tryghost/errors/-/errors-1.2.0.tgz#989f10434a17286e952b5a9434e50846ea4ad87c"
integrity sha512-80I7LmRgPQt380Bm/90hF8KPkkNqOFHF2T6oO+NXkLd+UTc1qLOfe6nZS17WD9glMmHrqv6IF8U1MjPMDa4VOQ==
dependencies:
lodash "^4.17.21"
uuid "^8.3.2"
@ -1428,6 +1428,14 @@
"@tryghost/ignition-errors" "^0.1.0"
lodash "^4.17.21"
"@tryghost/errors@^1.0.0", "@tryghost/errors@^1.1.0", "@tryghost/errors@^1.1.1":
version "1.1.1"
resolved "https://registry.npmjs.org/@tryghost/errors/-/errors-1.1.1.tgz"
integrity sha512-na0qB5sdy1BWgquzn+m530ohJ3fTeF451xUTR7I8b76TBEL9snnIkXCv5Qdjmnevmgod7aAGsHi2syyKFlvEvQ==
dependencies:
lodash "^4.17.21"
uuid "^8.3.2"
"@tryghost/express-dynamic-redirects@0.2.2":
version "0.2.2"
resolved "https://registry.npmjs.org/@tryghost/express-dynamic-redirects/-/express-dynamic-redirects-0.2.2.tgz"
@ -1712,6 +1720,14 @@
mobiledoc-dom-renderer "0.7.0"
mobiledoc-text-renderer "0.4.0"
"@tryghost/mw-error-handler@0.1.1":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@tryghost/mw-error-handler/-/mw-error-handler-0.1.1.tgz#47ad5f534b21ec71db00706f47622d660cde894b"
integrity sha512-c1EMdeU5k6FpB640GxjA2sUsyCuTPW8qlWJu1eZAbX/63YBMZOoBkvGNadUM5hdJ1fGwwFQME2j1vzkQ+qRMHg==
dependencies:
"@tryghost/debug" "^0.1.9"
"@tryghost/tpl" "^0.1.8"
"@tryghost/mw-session-from-token@0.1.26":
version "0.1.26"
resolved "https://registry.npmjs.org/@tryghost/mw-session-from-token/-/mw-session-from-token-0.1.26.tgz"