ghost 5.80.1 → 5.80.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/components/tryghost-adapter-cache-memory-ttl-5.80.3.tgz +0 -0
  2. package/components/{tryghost-adapter-cache-redis-5.80.1.tgz → tryghost-adapter-cache-redis-5.80.3.tgz} +0 -0
  3. package/components/{tryghost-adapter-manager-5.80.1.tgz → tryghost-adapter-manager-5.80.3.tgz} +0 -0
  4. package/components/tryghost-announcement-bar-settings-5.80.3.tgz +0 -0
  5. package/components/{tryghost-api-framework-5.80.1.tgz → tryghost-api-framework-5.80.3.tgz} +0 -0
  6. package/components/{tryghost-api-version-compatibility-service-5.80.1.tgz → tryghost-api-version-compatibility-service-5.80.3.tgz} +0 -0
  7. package/components/tryghost-audience-feedback-5.80.3.tgz +0 -0
  8. package/components/{tryghost-bookshelf-repository-5.80.1.tgz → tryghost-bookshelf-repository-5.80.3.tgz} +0 -0
  9. package/components/{tryghost-bootstrap-socket-5.80.1.tgz → tryghost-bootstrap-socket-5.80.3.tgz} +0 -0
  10. package/components/tryghost-collections-5.80.3.tgz +0 -0
  11. package/components/{tryghost-constants-5.80.1.tgz → tryghost-constants-5.80.3.tgz} +0 -0
  12. package/components/tryghost-custom-theme-settings-service-5.80.3.tgz +0 -0
  13. package/components/{tryghost-data-generator-5.80.1.tgz → tryghost-data-generator-5.80.3.tgz} +0 -0
  14. package/components/tryghost-domain-events-5.80.3.tgz +0 -0
  15. package/components/tryghost-donations-5.80.3.tgz +0 -0
  16. package/components/tryghost-dynamic-routing-events-5.80.3.tgz +0 -0
  17. package/components/tryghost-email-addresses-5.80.3.tgz +0 -0
  18. package/components/tryghost-email-analytics-provider-mailgun-5.80.3.tgz +0 -0
  19. package/components/tryghost-email-analytics-service-5.80.3.tgz +0 -0
  20. package/components/tryghost-email-content-generator-5.80.3.tgz +0 -0
  21. package/components/{tryghost-email-events-5.80.1.tgz → tryghost-email-events-5.80.3.tgz} +0 -0
  22. package/components/tryghost-email-service-5.80.3.tgz +0 -0
  23. package/components/tryghost-email-suppression-list-5.80.3.tgz +0 -0
  24. package/components/{tryghost-express-dynamic-redirects-5.80.1.tgz → tryghost-express-dynamic-redirects-5.80.3.tgz} +0 -0
  25. package/components/tryghost-external-media-inliner-5.80.3.tgz +0 -0
  26. package/components/tryghost-extract-api-key-5.80.3.tgz +0 -0
  27. package/components/tryghost-ghost-5.80.3.tgz +0 -0
  28. package/components/{tryghost-html-to-plaintext-5.80.1.tgz → tryghost-html-to-plaintext-5.80.3.tgz} +0 -0
  29. package/components/tryghost-i18n-5.80.3.tgz +0 -0
  30. package/components/tryghost-importer-handler-content-files-5.80.3.tgz +0 -0
  31. package/components/{tryghost-importer-revue-5.80.1.tgz → tryghost-importer-revue-5.80.3.tgz} +0 -0
  32. package/components/tryghost-in-memory-repository-5.80.3.tgz +0 -0
  33. package/components/tryghost-job-manager-5.80.3.tgz +0 -0
  34. package/components/tryghost-link-redirects-5.80.3.tgz +0 -0
  35. package/components/tryghost-link-replacer-5.80.3.tgz +0 -0
  36. package/components/tryghost-link-tracking-5.80.3.tgz +0 -0
  37. package/components/tryghost-magic-link-5.80.3.tgz +0 -0
  38. package/components/tryghost-mail-events-5.80.3.tgz +0 -0
  39. package/components/tryghost-mailgun-client-5.80.3.tgz +0 -0
  40. package/components/tryghost-member-attribution-5.80.3.tgz +0 -0
  41. package/components/{tryghost-member-events-5.80.1.tgz → tryghost-member-events-5.80.3.tgz} +0 -0
  42. package/components/tryghost-members-api-5.80.3.tgz +0 -0
  43. package/components/tryghost-members-csv-5.80.3.tgz +0 -0
  44. package/components/{tryghost-members-events-service-5.80.1.tgz → tryghost-members-events-service-5.80.3.tgz} +0 -0
  45. package/components/tryghost-members-importer-5.80.3.tgz +0 -0
  46. package/components/{tryghost-members-offers-5.80.1.tgz → tryghost-members-offers-5.80.3.tgz} +0 -0
  47. package/components/tryghost-members-payments-5.80.3.tgz +0 -0
  48. package/components/tryghost-members-ssr-5.80.3.tgz +0 -0
  49. package/components/{tryghost-members-stripe-service-5.80.1.tgz → tryghost-members-stripe-service-5.80.3.tgz} +0 -0
  50. package/components/tryghost-mentions-email-report-5.80.3.tgz +0 -0
  51. package/components/tryghost-milestones-5.80.3.tgz +0 -0
  52. package/components/tryghost-minifier-5.80.3.tgz +0 -0
  53. package/components/{tryghost-model-to-domain-event-interceptor-5.80.1.tgz → tryghost-model-to-domain-event-interceptor-5.80.3.tgz} +0 -0
  54. package/components/tryghost-mw-api-version-mismatch-5.80.3.tgz +0 -0
  55. package/components/tryghost-mw-cache-control-5.80.3.tgz +0 -0
  56. package/components/{tryghost-mw-error-handler-5.80.1.tgz → tryghost-mw-error-handler-5.80.3.tgz} +0 -0
  57. package/components/{tryghost-mw-session-from-token-5.80.1.tgz → tryghost-mw-session-from-token-5.80.3.tgz} +0 -0
  58. package/components/{tryghost-mw-update-user-last-seen-5.80.1.tgz → tryghost-mw-update-user-last-seen-5.80.3.tgz} +0 -0
  59. package/components/tryghost-mw-version-match-5.80.3.tgz +0 -0
  60. package/components/tryghost-mw-vhost-5.80.3.tgz +0 -0
  61. package/components/tryghost-nql-filter-expansions-5.80.3.tgz +0 -0
  62. package/components/tryghost-oembed-service-5.80.3.tgz +0 -0
  63. package/components/tryghost-package-json-5.80.3.tgz +0 -0
  64. package/components/{tryghost-post-events-5.80.1.tgz → tryghost-post-events-5.80.3.tgz} +0 -0
  65. package/components/tryghost-post-revisions-5.80.3.tgz +0 -0
  66. package/components/tryghost-posts-service-5.80.3.tgz +0 -0
  67. package/components/tryghost-recommendations-5.80.3.tgz +0 -0
  68. package/components/tryghost-referrers-5.80.3.tgz +0 -0
  69. package/components/{tryghost-security-5.80.1.tgz → tryghost-security-5.80.3.tgz} +0 -0
  70. package/components/tryghost-session-service-5.80.3.tgz +0 -0
  71. package/components/tryghost-settings-path-manager-5.80.3.tgz +0 -0
  72. package/components/tryghost-slack-notifications-5.80.3.tgz +0 -0
  73. package/components/{tryghost-staff-service-5.80.1.tgz → tryghost-staff-service-5.80.3.tgz} +0 -0
  74. package/components/{tryghost-stats-service-5.80.1.tgz → tryghost-stats-service-5.80.3.tgz} +0 -0
  75. package/components/tryghost-tiers-5.80.3.tgz +0 -0
  76. package/components/{tryghost-update-check-service-5.80.1.tgz → tryghost-update-check-service-5.80.3.tgz} +0 -0
  77. package/components/tryghost-verification-trigger-5.80.3.tgz +0 -0
  78. package/components/{tryghost-version-notifications-data-service-5.80.1.tgz → tryghost-version-notifications-data-service-5.80.3.tgz} +0 -0
  79. package/components/tryghost-webmentions-5.80.3.tgz +0 -0
  80. package/content/themes/casper/assets/built/screen.css +1 -1
  81. package/content/themes/casper/assets/built/screen.css.map +1 -1
  82. package/content/themes/casper/assets/css/screen.css +4 -0
  83. package/content/themes/casper/default.hbs +3 -3
  84. package/content/themes/casper/package.json +1 -1
  85. package/content/themes/casper/partials/post-card.hbs +1 -1
  86. package/content/themes/casper/post.hbs +3 -3
  87. package/content/themes/source/assets/built/screen.css +1 -1
  88. package/content/themes/source/assets/built/screen.css.map +1 -1
  89. package/content/themes/source/assets/built/source.js +1 -1
  90. package/content/themes/source/assets/built/source.js.map +1 -1
  91. package/content/themes/source/assets/css/screen.css +80 -33
  92. package/content/themes/source/assets/js/{casper.js → main.js} +14 -0
  93. package/content/themes/source/package.json +16 -1
  94. package/content/themes/source/partials/components/cta.hbs +1 -1
  95. package/content/themes/source/partials/components/footer.hbs +2 -2
  96. package/content/themes/source/partials/components/header-content.hbs +2 -2
  97. package/content/themes/source/partials/components/navigation.hbs +1 -1
  98. package/content/themes/source/partials/email-subscription.hbs +2 -2
  99. package/content/themes/source/partials/icons/lock.hbs +1 -5
  100. package/content/themes/source/partials/post-card.hbs +13 -3
  101. package/content/themes/source/post.hbs +18 -14
  102. package/core/boot.js +27 -0
  103. package/core/built/admin/assets/admin-x-demo/admin-x-demo.js +1 -1
  104. package/core/built/admin/assets/admin-x-demo/{index-0722d3aa.mjs → index-771f4964.mjs} +2 -2
  105. package/core/built/admin/assets/admin-x-demo/{modals-969dbbd3.mjs → modals-4c390700.mjs} +46 -46
  106. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-cd254a7a.mjs → CodeEditorView-00610c2f.mjs} +2 -2
  107. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +2 -2
  108. package/core/built/admin/assets/admin-x-settings/{index-b52632c4.mjs → index-2deb7a1f.mjs} +4273 -4908
  109. package/core/built/admin/assets/admin-x-settings/{index-613b352d.mjs → index-e308aac8.mjs} +2859 -2831
  110. package/core/built/admin/assets/admin-x-settings/{modals-99dbacc0.mjs → modals-e2ca767c.mjs} +13267 -10619
  111. package/core/built/admin/assets/{chunk.524.eecfc0cb2091306f132c.js → chunk.524.c6ed93ff44a9cb49235c.js} +6 -6
  112. package/core/built/admin/assets/{chunk.582.e04c74f002928b557602.js → chunk.582.4e31037f1eb4c88f017f.js} +4 -4
  113. package/core/built/admin/assets/{chunk.763.6b160e403073ecabbfa6.js → chunk.763.7849440ec6494d2449e9.js} +997 -1197
  114. package/core/built/admin/assets/{ghost-ea7a51891dfca9b7a04aacfffe7dea4e.js → ghost-594a7dd26bdb4fec210b285cab5555cb.js} +14 -14
  115. package/core/built/admin/assets/ghost-71cfcdf9410af9b3aec11a28f9a4130f.css +1 -0
  116. package/core/built/admin/assets/ghost-dark-1e5bec737be037b1b5cb2a423603271d.css +1 -0
  117. package/core/built/admin/assets/{vendor-6b83da0c6e6fede335947219c59e1543.js → vendor-43a631b7c834235c4ff0b2186b5ca27d.js} +7 -9
  118. package/core/built/admin/index.html +6 -6
  119. package/core/frontend/helpers/get.js +89 -2
  120. package/core/frontend/public/robots.txt +0 -1
  121. package/core/frontend/services/rendering/renderer.js +19 -1
  122. package/core/frontend/services/theme-engine/i18n/I18n.js +7 -6
  123. package/core/frontend/services/theme-engine/i18n/ThemeI18n.js +4 -3
  124. package/core/server/adapters/storage/LocalStorageBase.js +13 -1
  125. package/core/server/api/endpoints/images.js +14 -3
  126. package/core/server/api/endpoints/utils/serializers/input/tiers.js +30 -2
  127. package/core/server/services/auth/api-key/admin.js +92 -90
  128. package/core/server/services/members/service.js +3 -1
  129. package/core/server/services/slack-notifications/service.js +6 -3
  130. package/core/server/services/themes/installer.js +1 -1
  131. package/core/server/services/tiers/TierRepository.js +2 -1
  132. package/core/server/web/admin/app.js +18 -1
  133. package/core/server/web/api/endpoints/admin/app.js +10 -0
  134. package/core/shared/labs.js +1 -0
  135. package/package.json +152 -150
  136. package/yarn.lock +532 -324
  137. package/components/tryghost-adapter-cache-memory-ttl-5.80.1.tgz +0 -0
  138. package/components/tryghost-announcement-bar-settings-5.80.1.tgz +0 -0
  139. package/components/tryghost-audience-feedback-5.80.1.tgz +0 -0
  140. package/components/tryghost-collections-5.80.1.tgz +0 -0
  141. package/components/tryghost-custom-theme-settings-service-5.80.1.tgz +0 -0
  142. package/components/tryghost-domain-events-5.80.1.tgz +0 -0
  143. package/components/tryghost-donations-5.80.1.tgz +0 -0
  144. package/components/tryghost-dynamic-routing-events-5.80.1.tgz +0 -0
  145. package/components/tryghost-email-addresses-5.80.1.tgz +0 -0
  146. package/components/tryghost-email-analytics-provider-mailgun-5.80.1.tgz +0 -0
  147. package/components/tryghost-email-analytics-service-5.80.1.tgz +0 -0
  148. package/components/tryghost-email-content-generator-5.80.1.tgz +0 -0
  149. package/components/tryghost-email-service-5.80.1.tgz +0 -0
  150. package/components/tryghost-email-suppression-list-5.80.1.tgz +0 -0
  151. package/components/tryghost-external-media-inliner-5.80.1.tgz +0 -0
  152. package/components/tryghost-extract-api-key-5.80.1.tgz +0 -0
  153. package/components/tryghost-i18n-5.80.1.tgz +0 -0
  154. package/components/tryghost-importer-handler-content-files-5.80.1.tgz +0 -0
  155. package/components/tryghost-in-memory-repository-5.80.1.tgz +0 -0
  156. package/components/tryghost-job-manager-5.80.1.tgz +0 -0
  157. package/components/tryghost-link-redirects-5.80.1.tgz +0 -0
  158. package/components/tryghost-link-replacer-5.80.1.tgz +0 -0
  159. package/components/tryghost-link-tracking-5.80.1.tgz +0 -0
  160. package/components/tryghost-magic-link-5.80.1.tgz +0 -0
  161. package/components/tryghost-mail-events-5.80.1.tgz +0 -0
  162. package/components/tryghost-mailgun-client-5.80.1.tgz +0 -0
  163. package/components/tryghost-member-attribution-5.80.1.tgz +0 -0
  164. package/components/tryghost-members-api-5.80.1.tgz +0 -0
  165. package/components/tryghost-members-csv-5.80.1.tgz +0 -0
  166. package/components/tryghost-members-importer-5.80.1.tgz +0 -0
  167. package/components/tryghost-members-payments-5.80.1.tgz +0 -0
  168. package/components/tryghost-members-ssr-5.80.1.tgz +0 -0
  169. package/components/tryghost-mentions-email-report-5.80.1.tgz +0 -0
  170. package/components/tryghost-milestones-5.80.1.tgz +0 -0
  171. package/components/tryghost-minifier-5.80.1.tgz +0 -0
  172. package/components/tryghost-mw-api-version-mismatch-5.80.1.tgz +0 -0
  173. package/components/tryghost-mw-cache-control-5.80.1.tgz +0 -0
  174. package/components/tryghost-mw-version-match-5.80.1.tgz +0 -0
  175. package/components/tryghost-mw-vhost-5.80.1.tgz +0 -0
  176. package/components/tryghost-nql-filter-expansions-5.80.1.tgz +0 -0
  177. package/components/tryghost-oembed-service-5.80.1.tgz +0 -0
  178. package/components/tryghost-package-json-5.80.1.tgz +0 -0
  179. package/components/tryghost-post-revisions-5.80.1.tgz +0 -0
  180. package/components/tryghost-posts-service-5.80.1.tgz +0 -0
  181. package/components/tryghost-recommendations-5.80.1.tgz +0 -0
  182. package/components/tryghost-referrers-5.80.1.tgz +0 -0
  183. package/components/tryghost-session-service-5.80.1.tgz +0 -0
  184. package/components/tryghost-settings-path-manager-5.80.1.tgz +0 -0
  185. package/components/tryghost-slack-notifications-5.80.1.tgz +0 -0
  186. package/components/tryghost-tiers-5.80.1.tgz +0 -0
  187. package/components/tryghost-verification-trigger-5.80.1.tgz +0 -0
  188. package/components/tryghost-webmentions-5.80.1.tgz +0 -0
  189. package/core/built/admin/assets/ghost-40f01556960b49381176bbfa19a89c9a.css +0 -1
  190. package/core/built/admin/assets/ghost-dark-a86f0499bf2697104213d035da3a3b4d.css +0 -1
  191. /package/core/built/admin/assets/{chunk.763.6b160e403073ecabbfa6.js.LICENSE.txt → chunk.763.7849440ec6494d2449e9.js.LICENSE.txt} +0 -0
@@ -17,6 +17,7 @@ const messages = {
17
17
  };
18
18
 
19
19
  let JWT_OPTIONS_DEFAULTS = {
20
+ /** @type import('jsonwebtoken').Algorithm[] */
20
21
  algorithms: ['HS256'],
21
22
  maxAge: '5m'
22
23
  };
@@ -60,7 +61,7 @@ const authenticate = function apiKeyAdminAuth(req, res, next) {
60
61
  }));
61
62
  }
62
63
 
63
- return authenticateWithToken(req, res, next, {token, JWT_OPTIONS: JWT_OPTIONS_DEFAULTS});
64
+ return wrappedAuthenticateWithToken(req, res, next, {token});
64
65
  };
65
66
 
66
67
  const authenticateWithUrl = function apiKeyAuthenticateWithUrl(req, res, next) {
@@ -72,9 +73,20 @@ const authenticateWithUrl = function apiKeyAuthenticateWithUrl(req, res, next) {
72
73
  }));
73
74
  }
74
75
  // CASE: Scheduler publish URLs can have long maxAge but controllerd by expiry and neverBefore
75
- return authenticateWithToken(req, res, next, {token, JWT_OPTIONS: _.omit(JWT_OPTIONS_DEFAULTS, 'maxAge')});
76
+ return wrappedAuthenticateWithToken(req, res, next, {token, ignoreMaxAge: true});
76
77
  };
77
78
 
79
+ async function wrappedAuthenticateWithToken(req, res, next, options) {
80
+ try {
81
+ const {apiKey, user} = await authenticateWithToken(req.originalUrl, options.token, options.ignoreMaxAge);
82
+ req.api_key = apiKey;
83
+ req.user = user;
84
+ next();
85
+ } catch (err) {
86
+ next(err);
87
+ }
88
+ }
89
+
78
90
  /**
79
91
  * Admin API key authentication flow:
80
92
  * 1. extract the JWT token from the `Authorization: Ghost xxxx` header or from URL(for schedules)
@@ -89,114 +101,104 @@ const authenticateWithUrl = function apiKeyAuthenticateWithUrl(req, res, next) {
89
101
  * - the "Audience" claim should match the requested API path
90
102
  * https://tools.ietf.org/html/rfc7519#section-4.1.3
91
103
  */
92
- const authenticateWithToken = async function apiKeyAuthenticateWithToken(req, res, next, {token, JWT_OPTIONS}) {
104
+ const authenticateWithToken = async function apiKeyAuthenticateWithToken(originalUrl, token, ignoreMaxAge) {
93
105
  const decoded = jwt.decode(token, {complete: true});
106
+ const jwtValidationOptions = ignoreMaxAge ? _.omit(JWT_OPTIONS_DEFAULTS, 'maxAge') : JWT_OPTIONS_DEFAULTS;
94
107
 
95
108
  if (!decoded || !decoded.header) {
96
- return next(new errors.BadRequestError({
109
+ throw new errors.BadRequestError({
97
110
  message: tpl(messages.invalidToken),
98
111
  code: 'INVALID_JWT'
99
- }));
112
+ });
100
113
  }
101
114
 
102
115
  const apiKeyId = decoded.header.kid;
103
116
 
104
117
  if (!apiKeyId) {
105
- return next(new errors.BadRequestError({
118
+ throw new errors.BadRequestError({
106
119
  message: tpl(messages.adminApiKidMissing),
107
120
  code: 'MISSING_ADMIN_API_KID'
108
- }));
121
+ });
109
122
  }
110
123
 
111
- try {
112
- const apiKey = await models.ApiKey.findOne({id: apiKeyId}, {withRelated: ['integration']});
113
-
114
- if (!apiKey) {
115
- return next(new errors.UnauthorizedError({
116
- message: tpl(messages.unknownAdminApiKey),
117
- code: 'UNKNOWN_ADMIN_API_KEY'
118
- }));
119
- }
120
-
121
- if (apiKey.get('type') !== 'admin') {
122
- return next(new errors.UnauthorizedError({
123
- message: tpl(messages.invalidApiKeyType),
124
- code: 'INVALID_API_KEY_TYPE'
125
- }));
126
- }
127
-
128
- // CASE: blocking all non-internal: "custom" and "builtin" integration requests when the limit is reached
129
- if (limitService.isLimited('customIntegrations')
124
+ const apiKey = await models.ApiKey.findOne({id: apiKeyId}, {withRelated: ['integration']});
125
+
126
+ if (!apiKey) {
127
+ throw new errors.UnauthorizedError({
128
+ message: tpl(messages.unknownAdminApiKey),
129
+ code: 'UNKNOWN_ADMIN_API_KEY'
130
+ });
131
+ }
132
+
133
+ if (apiKey.get('type') !== 'admin') {
134
+ throw new errors.UnauthorizedError({
135
+ message: tpl(messages.invalidApiKeyType),
136
+ code: 'INVALID_API_KEY_TYPE'
137
+ });
138
+ }
139
+
140
+ // CASE: blocking all non-internal: "custom" and "builtin" integration requests when the limit is reached
141
+ if (limitService.isLimited('customIntegrations')
130
142
  && (apiKey.relations.integration && !['internal', 'core'].includes(apiKey.relations.integration.get('type')))) {
131
- // NOTE: using "checkWouldGoOverLimit" instead of "checkIsOverLimit" here because flag limits don't have
132
- // a concept of measuring if the limit has been surpassed
133
- await limitService.errorIfWouldGoOverLimit('customIntegrations');
134
- }
135
-
136
- // Decoding from hex and transforming into bytes is here to
137
- // keep comparison of the bytes that are stored in the secret.
138
- // Useful context:
139
- // https://github.com/auth0/node-jsonwebtoken/issues/208#issuecomment-231861138
140
- const secret = Buffer.from(apiKey.get('secret'), 'hex');
141
-
142
- // Using req.originalUrl means we get the right url even if version-rewrites have happened
143
- const {version, api} = legacyApiPathMatch(req.originalUrl);
144
-
145
- // ensure the token was meant for this api
146
- let options;
147
-
148
- if (version) {
149
- // CASE: legacy versioned api request
150
- options = Object.assign({
151
- audience: new RegExp(`/?${version}/${api}/?$`)
152
- }, JWT_OPTIONS);
153
- } else {
154
- options = Object.assign({
155
- audience: new RegExp(`/?${api}/?$`)
156
- }, JWT_OPTIONS);
157
- }
158
-
159
- try {
160
- jwt.verify(token, secret, options);
161
- } catch (err) {
162
- if (err.name === 'TokenExpiredError' || err.name === 'JsonWebTokenError') {
163
- return next(new errors.UnauthorizedError({
164
- message: tpl(messages.invalidTokenWithMessage, {message: err.message}),
165
- code: 'INVALID_JWT',
166
- err
167
- }));
168
- }
169
-
170
- // unknown error
171
- return next(new errors.InternalServerError({err}));
172
- }
173
-
174
- // authenticated OK
175
-
176
- if (apiKey.get('user_id')) {
177
- // fetch the user and store it on the request for later checks and logging
178
- const user = await models.User.findOne(
179
- {id: apiKey.get('user_id'), status: 'active'},
180
- {require: true}
181
- );
182
-
183
- req.user = user;
184
- }
185
-
186
- // store the api key on the request for later checks and logging
187
- req.api_key = apiKey;
143
+ // NOTE: using "checkWouldGoOverLimit" instead of "checkIsOverLimit" here because flag limits don't have
144
+ // a concept of measuring if the limit has been surpassed
145
+ await limitService.errorIfWouldGoOverLimit('customIntegrations');
146
+ }
188
147
 
189
- next();
148
+ // Decoding from hex and transforming into bytes is here to
149
+ // keep comparison of the bytes that are stored in the secret.
150
+ // Useful context:
151
+ // https://github.com/auth0/node-jsonwebtoken/issues/208#issuecomment-231861138
152
+ const secret = Buffer.from(apiKey.get('secret'), 'hex');
153
+
154
+ // Using req.originalUrl means we get the right url even if version-rewrites have happened
155
+ const {version, api} = legacyApiPathMatch(originalUrl);
156
+
157
+ // ensure the token was meant for this api
158
+ let options;
159
+
160
+ if (version) {
161
+ // CASE: legacy versioned api request
162
+ options = Object.assign({
163
+ audience: new RegExp(`/?${version}/${api}/?$`)
164
+ }, jwtValidationOptions);
165
+ } else {
166
+ options = Object.assign({
167
+ audience: new RegExp(`/?${api}/?$`)
168
+ }, jwtValidationOptions);
169
+ }
170
+
171
+ try {
172
+ jwt.verify(token, secret, options);
190
173
  } catch (err) {
191
- if (err instanceof errors.HostLimitError) {
192
- next(err);
193
- } else {
194
- next(new errors.InternalServerError({err}));
195
- }
174
+ throw new errors.UnauthorizedError({
175
+ message: tpl(messages.invalidTokenWithMessage, {message: err.message}),
176
+ code: 'INVALID_JWT',
177
+ err
178
+ });
179
+ }
180
+
181
+ // authenticated OK
182
+ let result = {
183
+ user: null,
184
+ apiKey: apiKey
185
+ };
186
+
187
+ if (apiKey.get('user_id')) {
188
+ // fetch the user and store it on the request for later checks and logging
189
+ const user = await models.User.findOne(
190
+ {id: apiKey.get('user_id'), status: 'active'},
191
+ {require: true}
192
+ );
193
+
194
+ result.user = user;
196
195
  }
196
+
197
+ return result;
197
198
  };
198
199
 
199
200
  module.exports = {
200
201
  authenticate,
201
- authenticateWithUrl
202
+ authenticateWithUrl,
203
+ authenticateWithToken
202
204
  };
@@ -55,7 +55,9 @@ const initMembersCSVImporter = ({stripeAPIService}) => {
55
55
  },
56
56
  getTierByName: async (name) => {
57
57
  const tiers = await tiersService.api.browse({
58
- filter: `name:'${name}'`
58
+ filter: {
59
+ name
60
+ }
59
61
  });
60
62
 
61
63
  if (tiers.data.length > 0) {
@@ -12,10 +12,11 @@ class SlackNotificationsServiceWrapper {
12
12
  * @param {string} deps.siteUrl
13
13
  * @param {boolean} deps.isEnabled
14
14
  * @param {URL} deps.webhookUrl
15
+ * @param {number} deps.minThreshold
15
16
  *
16
17
  * @returns {import('@tryghost/slack-notifications/lib/SlackNotificationsService')}
17
18
  */
18
- static create({siteUrl, isEnabled, webhookUrl}) {
19
+ static create({siteUrl, isEnabled, webhookUrl, minThreshold}) {
19
20
  const {
20
21
  SlackNotificationsService,
21
22
  SlackNotifications
@@ -32,7 +33,8 @@ class SlackNotificationsServiceWrapper {
32
33
  logging,
33
34
  config: {
34
35
  isEnabled,
35
- webhookUrl
36
+ webhookUrl,
37
+ minThreshold
36
38
  },
37
39
  slackNotifications
38
40
  });
@@ -49,8 +51,9 @@ class SlackNotificationsServiceWrapper {
49
51
  const siteUrl = urlUtils.getSiteUrl();
50
52
  const isEnabled = !!(hostSettings?.milestones?.enabled && hostSettings?.milestones?.url);
51
53
  const webhookUrl = hostSettings?.milestones?.url;
54
+ const minThreshold = hostSettings?.milestones?.minThreshold ? parseInt(hostSettings.milestones.minThreshold) : 0;
52
55
 
53
- this.#api = SlackNotificationsServiceWrapper.create({siteUrl, isEnabled, webhookUrl});
56
+ this.#api = SlackNotificationsServiceWrapper.create({siteUrl, isEnabled, webhookUrl, minThreshold});
54
57
 
55
58
  this.#api.subscribeEvents();
56
59
  }
@@ -3,7 +3,7 @@ const os = require('os');
3
3
  const path = require('path');
4
4
  const security = require('@tryghost/security');
5
5
  const request = require('@tryghost/request');
6
- const errors = require('@tryghost/errors/lib/errors');
6
+ const errors = require('@tryghost/errors');
7
7
  const limitService = require('../../services/limits');
8
8
  const {setFromZip} = require('./storage');
9
9
 
@@ -86,7 +86,8 @@ module.exports = class TierRepository {
86
86
  * @returns {Promise<import('@tryghost/tiers/lib/Tier')[]>}
87
87
  */
88
88
  async getAll(options = {}) {
89
- const filter = nql(options.filter, {});
89
+ const filter = nql();
90
+ filter.filter = options.filter || {};
90
91
  return Promise.all(this.#store.slice().filter((item) => {
91
92
  return filter.queryJSON(this.toPrimitive(item));
92
93
  }).map((tier) => {
@@ -30,7 +30,24 @@ module.exports = function setupAdminApp() {
30
30
  }
31
31
  ));
32
32
 
33
- adminApp.use('/auth-frame', serveStatic(
33
+ // Auth Frame renders a HTML page that loads some JS which then makes an API
34
+ // request to the Admin API /users/me/ endpoint to check if the user is logged in.
35
+ //
36
+ // Used by comments-ui to add moderation options to front-end comments when logged in.
37
+ adminApp.use('/auth-frame', (req, res, next) => {
38
+ // only render content when we have an Admin session cookie,
39
+ // otherwise return a 204 to avoid JS and API requests being made unnecessarily
40
+ try {
41
+ if (req.headers.cookie?.includes('ghost-admin-api-session')) {
42
+ next();
43
+ } else {
44
+ res.setHeader('Cache-Control', 'public, max-age=0');
45
+ res.sendStatus(204);
46
+ }
47
+ } catch (err) {
48
+ next(err);
49
+ }
50
+ }, serveStatic(
34
51
  path.join(config.getContentPath('public'), 'admin-auth')
35
52
  ));
36
53
 
@@ -4,11 +4,13 @@ const bodyParser = require('body-parser');
4
4
  const errorHandler = require('@tryghost/mw-error-handler');
5
5
  const versionMatch = require('@tryghost/mw-version-match');
6
6
 
7
+ const labs = require('../../../../../shared/labs');
7
8
  const shared = require('../../../shared');
8
9
  const express = require('../../../../../shared/express');
9
10
  const sentry = require('../../../../../shared/sentry');
10
11
  const routes = require('./routes');
11
12
  const APIVersionCompatibilityService = require('../../../../services/api-version-compatibility');
13
+ const GhostNestApp = require('@tryghost/ghost');
12
14
 
13
15
  module.exports = function setupApiApp() {
14
16
  debug('Admin API setup start');
@@ -33,6 +35,14 @@ module.exports = function setupApiApp() {
33
35
  // Routing
34
36
  apiApp.use(routes());
35
37
 
38
+ apiApp.use(async (req, res, next) => {
39
+ if (!labs.isSet('NestPlayground')) {
40
+ return next();
41
+ }
42
+ const app = await GhostNestApp.getApp();
43
+ app.getHttpAdapter().getInstance()(req, res, next);
44
+ });
45
+
36
46
  // API error handling
37
47
  apiApp.use(errorHandler.resourceNotFound);
38
48
  apiApp.use(APIVersionCompatibilityService.errorHandler);
@@ -36,6 +36,7 @@ const BETA_FEATURES = [
36
36
  ];
37
37
 
38
38
  const ALPHA_FEATURES = [
39
+ 'NestPlayground',
39
40
  'urlCache',
40
41
  'lexicalMultiplayer',
41
42
  'websockets',