ghost 5.82.10 → 5.82.12

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 (149) hide show
  1. package/components/{tryghost-adapter-cache-memory-ttl-5.82.10.tgz → tryghost-adapter-cache-memory-ttl-5.82.12.tgz} +0 -0
  2. package/components/{tryghost-adapter-cache-redis-5.82.10.tgz → tryghost-adapter-cache-redis-5.82.12.tgz} +0 -0
  3. package/components/{tryghost-adapter-manager-5.82.10.tgz → tryghost-adapter-manager-5.82.12.tgz} +0 -0
  4. package/components/tryghost-announcement-bar-settings-5.82.12.tgz +0 -0
  5. package/components/tryghost-api-framework-5.82.12.tgz +0 -0
  6. package/components/{tryghost-api-version-compatibility-service-5.82.10.tgz → tryghost-api-version-compatibility-service-5.82.12.tgz} +0 -0
  7. package/components/{tryghost-audience-feedback-5.82.10.tgz → tryghost-audience-feedback-5.82.12.tgz} +0 -0
  8. package/components/tryghost-bookshelf-repository-5.82.12.tgz +0 -0
  9. package/components/tryghost-bootstrap-socket-5.82.12.tgz +0 -0
  10. package/components/tryghost-collections-5.82.12.tgz +0 -0
  11. package/components/tryghost-constants-5.82.12.tgz +0 -0
  12. package/components/tryghost-custom-theme-settings-service-5.82.12.tgz +0 -0
  13. package/components/{tryghost-data-generator-5.82.10.tgz → tryghost-data-generator-5.82.12.tgz} +0 -0
  14. package/components/tryghost-domain-events-5.82.12.tgz +0 -0
  15. package/components/{tryghost-donations-5.82.10.tgz → tryghost-donations-5.82.12.tgz} +0 -0
  16. package/components/tryghost-dynamic-routing-events-5.82.12.tgz +0 -0
  17. package/components/{tryghost-email-addresses-5.82.10.tgz → tryghost-email-addresses-5.82.12.tgz} +0 -0
  18. package/components/{tryghost-email-analytics-provider-mailgun-5.82.10.tgz → tryghost-email-analytics-provider-mailgun-5.82.12.tgz} +0 -0
  19. package/components/{tryghost-email-analytics-service-5.82.10.tgz → tryghost-email-analytics-service-5.82.12.tgz} +0 -0
  20. package/components/{tryghost-email-content-generator-5.82.10.tgz → tryghost-email-content-generator-5.82.12.tgz} +0 -0
  21. package/components/{tryghost-email-events-5.82.10.tgz → tryghost-email-events-5.82.12.tgz} +0 -0
  22. package/components/{tryghost-email-service-5.82.10.tgz → tryghost-email-service-5.82.12.tgz} +0 -0
  23. package/components/{tryghost-email-suppression-list-5.82.10.tgz → tryghost-email-suppression-list-5.82.12.tgz} +0 -0
  24. package/components/{tryghost-express-dynamic-redirects-5.82.10.tgz → tryghost-express-dynamic-redirects-5.82.12.tgz} +0 -0
  25. package/components/{tryghost-external-media-inliner-5.82.10.tgz → tryghost-external-media-inliner-5.82.12.tgz} +0 -0
  26. package/components/tryghost-extract-api-key-5.82.12.tgz +0 -0
  27. package/components/tryghost-ghost-5.82.12.tgz +0 -0
  28. package/components/tryghost-html-to-plaintext-5.82.12.tgz +0 -0
  29. package/components/tryghost-i18n-5.82.12.tgz +0 -0
  30. package/components/tryghost-importer-handler-content-files-5.82.12.tgz +0 -0
  31. package/components/{tryghost-importer-revue-5.82.10.tgz → tryghost-importer-revue-5.82.12.tgz} +0 -0
  32. package/components/tryghost-in-memory-repository-5.82.12.tgz +0 -0
  33. package/components/{tryghost-job-manager-5.82.10.tgz → tryghost-job-manager-5.82.12.tgz} +0 -0
  34. package/components/{tryghost-link-redirects-5.82.10.tgz → tryghost-link-redirects-5.82.12.tgz} +0 -0
  35. package/components/{tryghost-link-replacer-5.82.10.tgz → tryghost-link-replacer-5.82.12.tgz} +0 -0
  36. package/components/{tryghost-link-tracking-5.82.10.tgz → tryghost-link-tracking-5.82.12.tgz} +0 -0
  37. package/components/{tryghost-magic-link-5.82.10.tgz → tryghost-magic-link-5.82.12.tgz} +0 -0
  38. package/components/{tryghost-mail-events-5.82.10.tgz → tryghost-mail-events-5.82.12.tgz} +0 -0
  39. package/components/{tryghost-mailgun-client-5.82.10.tgz → tryghost-mailgun-client-5.82.12.tgz} +0 -0
  40. package/components/{tryghost-member-attribution-5.82.10.tgz → tryghost-member-attribution-5.82.12.tgz} +0 -0
  41. package/components/{tryghost-member-events-5.82.10.tgz → tryghost-member-events-5.82.12.tgz} +0 -0
  42. package/components/tryghost-members-api-5.82.12.tgz +0 -0
  43. package/components/{tryghost-members-csv-5.82.10.tgz → tryghost-members-csv-5.82.12.tgz} +0 -0
  44. package/components/{tryghost-members-events-service-5.82.10.tgz → tryghost-members-events-service-5.82.12.tgz} +0 -0
  45. package/components/{tryghost-members-importer-5.82.10.tgz → tryghost-members-importer-5.82.12.tgz} +0 -0
  46. package/components/{tryghost-members-offers-5.82.10.tgz → tryghost-members-offers-5.82.12.tgz} +0 -0
  47. package/components/{tryghost-members-payments-5.82.10.tgz → tryghost-members-payments-5.82.12.tgz} +0 -0
  48. package/components/tryghost-members-ssr-5.82.12.tgz +0 -0
  49. package/components/{tryghost-members-stripe-service-5.82.10.tgz → tryghost-members-stripe-service-5.82.12.tgz} +0 -0
  50. package/components/{tryghost-mentions-email-report-5.82.10.tgz → tryghost-mentions-email-report-5.82.12.tgz} +0 -0
  51. package/components/tryghost-milestones-5.82.12.tgz +0 -0
  52. package/components/{tryghost-minifier-5.82.10.tgz → tryghost-minifier-5.82.12.tgz} +0 -0
  53. package/components/{tryghost-model-to-domain-event-interceptor-5.82.10.tgz → tryghost-model-to-domain-event-interceptor-5.82.12.tgz} +0 -0
  54. package/components/tryghost-mw-api-version-mismatch-5.82.12.tgz +0 -0
  55. package/components/tryghost-mw-cache-control-5.82.12.tgz +0 -0
  56. package/components/tryghost-mw-error-handler-5.82.12.tgz +0 -0
  57. package/components/{tryghost-mw-session-from-token-5.82.10.tgz → tryghost-mw-session-from-token-5.82.12.tgz} +0 -0
  58. package/components/tryghost-mw-update-user-last-seen-5.82.12.tgz +0 -0
  59. package/components/tryghost-mw-version-match-5.82.12.tgz +0 -0
  60. package/components/tryghost-mw-vhost-5.82.12.tgz +0 -0
  61. package/components/tryghost-nql-filter-expansions-5.82.12.tgz +0 -0
  62. package/components/{tryghost-oembed-service-5.82.10.tgz → tryghost-oembed-service-5.82.12.tgz} +0 -0
  63. package/components/tryghost-package-json-5.82.12.tgz +0 -0
  64. package/components/tryghost-post-events-5.82.12.tgz +0 -0
  65. package/components/{tryghost-post-revisions-5.82.10.tgz → tryghost-post-revisions-5.82.12.tgz} +0 -0
  66. package/components/tryghost-posts-service-5.82.12.tgz +0 -0
  67. package/components/{tryghost-recommendations-5.82.10.tgz → tryghost-recommendations-5.82.12.tgz} +0 -0
  68. package/components/tryghost-referrers-5.82.12.tgz +0 -0
  69. package/components/{tryghost-security-5.82.10.tgz → tryghost-security-5.82.12.tgz} +0 -0
  70. package/components/tryghost-session-service-5.82.12.tgz +0 -0
  71. package/components/tryghost-settings-path-manager-5.82.12.tgz +0 -0
  72. package/components/tryghost-slack-notifications-5.82.12.tgz +0 -0
  73. package/components/{tryghost-staff-service-5.82.10.tgz → tryghost-staff-service-5.82.12.tgz} +0 -0
  74. package/components/{tryghost-stats-service-5.82.10.tgz → tryghost-stats-service-5.82.12.tgz} +0 -0
  75. package/components/tryghost-tiers-5.82.12.tgz +0 -0
  76. package/components/{tryghost-update-check-service-5.82.10.tgz → tryghost-update-check-service-5.82.12.tgz} +0 -0
  77. package/components/{tryghost-verification-trigger-5.82.10.tgz → tryghost-verification-trigger-5.82.12.tgz} +0 -0
  78. package/components/{tryghost-version-notifications-data-service-5.82.10.tgz → tryghost-version-notifications-data-service-5.82.12.tgz} +0 -0
  79. package/components/{tryghost-webmentions-5.82.10.tgz → tryghost-webmentions-5.82.12.tgz} +0 -0
  80. package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +128 -127
  81. package/core/built/admin/assets/admin-x-demo/admin-x-demo.js +2 -2
  82. package/core/built/admin/assets/admin-x-demo/{index-f544d6cb.mjs → index-672397d7.mjs} +1082 -1054
  83. package/core/built/admin/assets/admin-x-demo/{modals-6544d395.mjs → modals-e68b00c5.mjs} +2 -2
  84. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-a1b9ba69.mjs → CodeEditorView-e6ecff28.mjs} +2 -2
  85. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +2 -2
  86. package/core/built/admin/assets/admin-x-settings/{index-d54677fb.mjs → index-3a124f37.mjs} +3015 -2975
  87. package/core/built/admin/assets/admin-x-settings/{index-8d83c7e9.mjs → index-ee4ab0db.mjs} +3586 -3427
  88. package/core/built/admin/assets/admin-x-settings/{modals-73b6c803.mjs → modals-8b47269b.mjs} +4962 -4997
  89. package/core/built/admin/assets/{chunk.799.5bcf6feef26f9fe94f0a.js → chunk.300.07bd40373843db34827c.js} +1177 -802
  90. package/core/built/admin/assets/{chunk.524.7bc3c52f480b7779363e.js → chunk.524.d0e760b5fbf40755f861.js} +5 -5
  91. package/core/built/admin/assets/{chunk.582.596e95cb181ba9f41b4e.js → chunk.582.b5b5252c47ef49f8f87a.js} +4 -4
  92. package/core/built/admin/assets/ghost-67652794f5902e84ccb0c67948563c1e.css +1 -0
  93. package/core/built/admin/assets/{ghost-377ce682ddf6e53f0ccb91aeae235f29.js → ghost-6f45467fa855b0c1da51c5f6c31f4744.js} +161 -143
  94. package/core/built/admin/assets/ghost-dark-e0cbaf72cee27f9de1eff5aea1152c46.css +1 -0
  95. package/core/built/admin/assets/koenig-lexical/index.css +1 -1
  96. package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +19043 -18668
  97. package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +134 -134
  98. package/core/built/admin/assets/{vendor-96853bcc300bd0b0cda2913c8edb0d14.js → vendor-0d51bbe51e0393cca143d7b52d073d8a.js} +44 -33
  99. package/core/built/admin/index.html +6 -6
  100. package/core/frontend/public/ghost.css +13 -2
  101. package/core/frontend/public/ghost.min.css +1 -1
  102. package/core/frontend/utils/images.js +4 -0
  103. package/core/frontend/web/middleware/frontend-caching.js +77 -0
  104. package/core/frontend/web/middleware/index.js +1 -0
  105. package/core/frontend/web/site.js +32 -14
  106. package/core/server/api/endpoints/utils/serializers/input/posts.js +40 -1
  107. package/core/server/services/members/middleware.js +65 -0
  108. package/core/server/services/offers/OfferBookshelfRepository.js +3 -3
  109. package/core/server/views/maintenance.html +1 -0
  110. package/core/server/web/members/app.js +8 -1
  111. package/core/shared/sentry.js +0 -1
  112. package/package.json +161 -160
  113. package/yarn.lock +708 -636
  114. package/components/tryghost-announcement-bar-settings-5.82.10.tgz +0 -0
  115. package/components/tryghost-api-framework-5.82.10.tgz +0 -0
  116. package/components/tryghost-bookshelf-repository-5.82.10.tgz +0 -0
  117. package/components/tryghost-bootstrap-socket-5.82.10.tgz +0 -0
  118. package/components/tryghost-collections-5.82.10.tgz +0 -0
  119. package/components/tryghost-constants-5.82.10.tgz +0 -0
  120. package/components/tryghost-custom-theme-settings-service-5.82.10.tgz +0 -0
  121. package/components/tryghost-domain-events-5.82.10.tgz +0 -0
  122. package/components/tryghost-dynamic-routing-events-5.82.10.tgz +0 -0
  123. package/components/tryghost-extract-api-key-5.82.10.tgz +0 -0
  124. package/components/tryghost-ghost-5.82.10.tgz +0 -0
  125. package/components/tryghost-html-to-plaintext-5.82.10.tgz +0 -0
  126. package/components/tryghost-i18n-5.82.10.tgz +0 -0
  127. package/components/tryghost-importer-handler-content-files-5.82.10.tgz +0 -0
  128. package/components/tryghost-in-memory-repository-5.82.10.tgz +0 -0
  129. package/components/tryghost-members-api-5.82.10.tgz +0 -0
  130. package/components/tryghost-members-ssr-5.82.10.tgz +0 -0
  131. package/components/tryghost-milestones-5.82.10.tgz +0 -0
  132. package/components/tryghost-mw-api-version-mismatch-5.82.10.tgz +0 -0
  133. package/components/tryghost-mw-cache-control-5.82.10.tgz +0 -0
  134. package/components/tryghost-mw-error-handler-5.82.10.tgz +0 -0
  135. package/components/tryghost-mw-update-user-last-seen-5.82.10.tgz +0 -0
  136. package/components/tryghost-mw-version-match-5.82.10.tgz +0 -0
  137. package/components/tryghost-mw-vhost-5.82.10.tgz +0 -0
  138. package/components/tryghost-nql-filter-expansions-5.82.10.tgz +0 -0
  139. package/components/tryghost-package-json-5.82.10.tgz +0 -0
  140. package/components/tryghost-post-events-5.82.10.tgz +0 -0
  141. package/components/tryghost-posts-service-5.82.10.tgz +0 -0
  142. package/components/tryghost-referrers-5.82.10.tgz +0 -0
  143. package/components/tryghost-session-service-5.82.10.tgz +0 -0
  144. package/components/tryghost-settings-path-manager-5.82.10.tgz +0 -0
  145. package/components/tryghost-slack-notifications-5.82.10.tgz +0 -0
  146. package/components/tryghost-tiers-5.82.10.tgz +0 -0
  147. package/core/built/admin/assets/ghost-dark-330dba70f72cf7c29a9c02c0fdef7377.css +0 -1
  148. package/core/built/admin/assets/ghost-e031f7ab21c73aa9a2a05ef2703e39cb.css +0 -1
  149. /package/core/built/admin/assets/{chunk.799.5bcf6feef26f9fe94f0a.js.LICENSE.txt → chunk.300.07bd40373843db34827c.js.LICENSE.txt} +0 -0
@@ -22,6 +22,7 @@ const shared = require('../../server/web/shared');
22
22
  const errorHandler = require('@tryghost/mw-error-handler');
23
23
  const mw = require('./middleware');
24
24
  const labs = require('../../shared/labs');
25
+ const bodyParser = require('body-parser');
25
26
 
26
27
  const STATIC_IMAGE_URL_PREFIX = `/${urlUtils.STATIC_IMAGE_URL_PREFIX}`;
27
28
  const STATIC_MEDIA_URL_PREFIX = `/${constants.STATIC_MEDIA_URL_PREFIX}`;
@@ -50,6 +51,21 @@ module.exports = function setupSiteApp(routerConfig) {
50
51
  // enable CORS headers (allows admin client to hit front-end when configured on separate URLs)
51
52
  siteApp.use(mw.cors);
52
53
 
54
+ const jsonParser = bodyParser.json({
55
+ type: ['application/activity+json', 'application/ld+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'],
56
+ // TODO: The @RawBody decorator in nest isn't working without this atm...
57
+ verify: function (req, res, buf) {
58
+ req.rawBody = buf;
59
+ }
60
+ });
61
+ siteApp.use(async function nestBodyParser(req, res, next) {
62
+ if (labs.isSet('NestPlayground') || labs.isSet('ActivityPub')) {
63
+ jsonParser(req, res, next);
64
+ return;
65
+ }
66
+ return next();
67
+ });
68
+
53
69
  siteApp.use(async function nestApp(req, res, next) {
54
70
  if (labs.isSet('NestPlayground') || labs.isSet('ActivityPub')) {
55
71
  const originalExpressApp = req.app;
@@ -105,9 +121,6 @@ module.exports = function setupSiteApp(routerConfig) {
105
121
  // Serve site files using the storage adapter
106
122
  siteApp.use(STATIC_FILES_URL_PREFIX, storage.getStorage('files').serve());
107
123
 
108
- // Global handling for member session, ensures a member is logged in to the frontend
109
- siteApp.use(membersService.middleware.loadMemberSession);
110
-
111
124
  // /member/.well-known/* serves files (e.g. jwks.json) so it needs to be mounted before the prettyUrl mw to avoid trailing slashes
112
125
  siteApp.use(
113
126
  '/members/.well-known',
@@ -132,18 +145,23 @@ module.exports = function setupSiteApp(routerConfig) {
132
145
 
133
146
  // Theme static assets/files
134
147
  siteApp.use(mw.staticTheme());
148
+
149
+ // Serve robots.txt if not found in theme
150
+ siteApp.use(mw.servePublicFile('static', 'robots.txt', 'text/plain', config.get('caching:robotstxt:maxAge')));
151
+
135
152
  debug('Static content done');
136
153
 
154
+ // site map - this should probably be refactored to be an internal app
155
+ sitemapHandler(siteApp);
156
+
157
+ // Global handling for member session, ensures a member is logged in to the frontend
158
+ siteApp.use(membersService.middleware.loadMemberSession);
159
+
137
160
  // Theme middleware
138
161
  // This should happen AFTER any shared assets are served, as it only changes things to do with templates
139
162
  siteApp.use(themeMiddleware);
140
163
  debug('Themes done');
141
164
 
142
- // Serve robots.txt if not found in theme
143
- siteApp.use(mw.servePublicFile('static', 'robots.txt', 'text/plain', config.get('caching:robotstxt:maxAge')));
144
-
145
- // site map - this should probably be refactored to be an internal app
146
- sitemapHandler(siteApp);
147
165
  debug('Internal apps done');
148
166
 
149
167
  // Add in all trailing slashes & remove uppercase
@@ -151,12 +169,12 @@ module.exports = function setupSiteApp(routerConfig) {
151
169
  siteApp.use(shared.middleware.prettyUrls);
152
170
 
153
171
  // ### Caching
154
- siteApp.use(function frontendCaching(req, res, next) {
155
- // Site frontend is cacheable UNLESS request made by a member or site is in private mode
156
- if (req.member || res.isPrivateBlog) {
157
- return shared.middleware.cacheControl('private')(req, res, next);
158
- } else {
159
- return shared.middleware.cacheControl('public', {maxAge: config.get('caching:frontend:maxAge')})(req, res, next);
172
+ siteApp.use(async function frontendCaching(req, res, next) {
173
+ try {
174
+ const middleware = await mw.frontendCaching.getMiddleware();
175
+ return middleware(req, res, next);
176
+ } catch {
177
+ return next();
160
178
  }
161
179
  });
162
180
 
@@ -7,6 +7,7 @@ const slugFilterOrder = require('./utils/slug-filter-order');
7
7
  const localUtils = require('../../index');
8
8
  const mobiledoc = require('../../../../../lib/mobiledoc');
9
9
  const postsMetaSchema = require('../../../../../data/schema').tables.posts_meta;
10
+ const postsSchema = require('../../../../../data/schema').tables.posts;
10
11
  const clean = require('./utils/clean');
11
12
  const lexical = require('../../../../../lib/lexical');
12
13
  const sentry = require('../../../../../../shared/sentry');
@@ -16,6 +17,16 @@ const messages = {
16
17
  failedHtmlToLexical: 'Failed to convert HTML to Lexical'
17
18
  };
18
19
 
20
+ /**
21
+ * Selects all allowed columns for the given frame.
22
+ *
23
+ * NOTE: This doesn't stop them from being FETCHED, just returned in the response. This causes
24
+ * the output serializer to remove them from the data object before returning.
25
+ *
26
+ * NOTE: This is only intended for the Content API. We need these fields within Admin API responses.
27
+ *
28
+ * @param {Object} frame - The frame object.
29
+ */
19
30
  function removeSourceFormats(frame) {
20
31
  if (frame.options.formats?.includes('mobiledoc') || frame.options.formats?.includes('lexical')) {
21
32
  frame.options.formats = frame.options.formats.filter((format) => {
@@ -24,6 +35,33 @@ function removeSourceFormats(frame) {
24
35
  }
25
36
  }
26
37
 
38
+ /**
39
+ * Selects all allowed columns for the given frame.
40
+ *
41
+ * This removes the lexical and mobiledoc columns from the query. This is a performance improvement as we never intend
42
+ * to expose those columns in the content API and they are very large datasets to be passing around and de/serializing.
43
+ *
44
+ * NOTE: This is only intended for the Content API. We need these fields within Admin API responses.
45
+ *
46
+ * @param {Object} frame - The frame object.
47
+ */
48
+ function selectAllAllowedColumns(frame) {
49
+ if (!frame.options.columns && !frame.options.selectRaw) {
50
+ // Because we're returning columns directly from the table we need to remove info columns like @@UNIQUE_CONSTRAINTS@@
51
+ frame.options.selectRaw = _.keys(_.omit(postsSchema, ['lexical','mobiledoc','@@UNIQUE_CONSTRAINTS@@'])).join(',');
52
+ } else if (frame.options.columns) {
53
+ frame.options.columns = frame.options.columns.filter((column) => {
54
+ return !['mobiledoc', 'lexical'].includes(column);
55
+ });
56
+ } else if (frame.options.selectRaw) {
57
+ frame.options.selectRaw = frame.options.selectRaw.split(',').map((column) => {
58
+ return column.trim();
59
+ }).filter((column) => {
60
+ return !['mobiledoc', 'lexical'].includes(column);
61
+ }).join(',');
62
+ }
63
+ }
64
+
27
65
  /**
28
66
  * Map names of relations to the internal names
29
67
  */
@@ -128,7 +166,8 @@ module.exports = {
128
166
  */
129
167
  if (localUtils.isContentAPI(frame)) {
130
168
  // CASE: the content api endpoint for posts should not return mobiledoc or lexical
131
- removeSourceFormats(frame);
169
+ removeSourceFormats(frame); // remove from the format field
170
+ selectAllAllowedColumns(frame); // remove from any specified column or selectRaw options
132
171
 
133
172
  setDefaultOrder(frame);
134
173
  forceVisibilityColumn(frame);
@@ -1,3 +1,4 @@
1
+ const crypto = require('crypto');
1
2
  const _ = require('lodash');
2
3
  const logging = require('@tryghost/logging');
3
4
  const membersService = require('./service');
@@ -11,12 +12,65 @@ const {
11
12
  } = require('./utils');
12
13
  const errors = require('@tryghost/errors');
13
14
  const tpl = require('@tryghost/tpl');
15
+ const onHeaders = require('on-headers');
16
+ const tiersService = require('../tiers/service');
17
+ const config = require('../../../shared/config');
14
18
 
15
19
  const messages = {
16
20
  missingUuid: 'Missing uuid.',
17
21
  invalidUuid: 'Invalid uuid.'
18
22
  };
19
23
 
24
+ const getFreeTier = async function getFreeTier() {
25
+ const response = await tiersService.api.browse();
26
+ const freeTier = response.data.find(tier => tier.type === 'free');
27
+ return freeTier;
28
+ };
29
+
30
+ /**
31
+ * Sets the ghost-access and ghost-access-hmac cookies on the response object
32
+ * @param {object} member - The member object
33
+ * @param {import('express').Response} res - The express response object to set the cookies on
34
+ * @returns
35
+ */
36
+ const setAccessCookies = function setAccessCookies(member = undefined, res, freeTier) {
37
+ if (!member) {
38
+ const accessCookie = `ghost-access=null; Max-Age=0; Path=/; HttpOnly; SameSite=Strict;`;
39
+ const hmacCookie = `ghost-access-hmac=null; Max-Age=0; Path=/; HttpOnly; SameSite=Strict;`;
40
+ const existingCookies = res.getHeader('Set-Cookie') || [];
41
+ const cookiesToSet = [accessCookie, hmacCookie].concat(existingCookies);
42
+
43
+ res.setHeader('Set-Cookie', cookiesToSet);
44
+ return;
45
+ }
46
+ const hmacSecret = config.get('cacheMembersContent:hmacSecret');
47
+ if (!hmacSecret) {
48
+ return;
49
+ }
50
+ const activeSubscription = member.subscriptions?.find(sub => sub.status === 'active');
51
+
52
+ const cookieTimestamp = Math.floor(Date.now() / 1000); // to mitigate a cookie replay attack
53
+ const memberTier = activeSubscription && activeSubscription.tier.id || freeTier.id;
54
+ const memberTierAndTimestamp = `${memberTier}:${cookieTimestamp}`;
55
+ const memberTierHmac = crypto.createHmac('sha256', hmacSecret).update(memberTierAndTimestamp).digest('hex');
56
+
57
+ const maxAge = 3600;
58
+ const accessCookie = `ghost-access=${memberTierAndTimestamp}; Max-Age=${maxAge}; Path=/; HttpOnly; SameSite=Strict;`;
59
+ const hmacCookie = `ghost-access-hmac=${memberTierHmac}; Max-Age=${maxAge}; Path=/; HttpOnly; SameSite=Strict;`;
60
+
61
+ const existingCookies = res.getHeader('Set-Cookie') || [];
62
+ const cookiesToSet = [accessCookie, hmacCookie].concat(existingCookies);
63
+ res.setHeader('Set-Cookie', cookiesToSet);
64
+ };
65
+
66
+ const accessInfoSession = async function accessInfoSession(req, res, next) {
67
+ const freeTier = await getFreeTier();
68
+ onHeaders(res, function () {
69
+ setAccessCookies(req.member, res, freeTier);
70
+ });
71
+ next();
72
+ };
73
+
20
74
  // @TODO: This piece of middleware actually belongs to the frontend, not to the member app
21
75
  // Need to figure a way to separate these things (e.g. frontend actually talks to members API)
22
76
  const loadMemberSession = async function loadMemberSession(req, res, next) {
@@ -242,6 +296,16 @@ const createSessionFromMagicLink = async function createSessionFromMagicLink(req
242
296
  // Note: don't reset 'member_login', or that would give an easy way around user enumeration by logging in to a manually created account
243
297
  const subscriptions = member && member.subscriptions || [];
244
298
 
299
+ if (config.get('cacheMembersContent:enabled')) {
300
+ // Set the ghost-access cookies to enable tier-based caching
301
+ try {
302
+ const freeTier = await getFreeTier();
303
+ setAccessCookies(member, res, freeTier);
304
+ } catch {
305
+ // This is a non-critical operation, so we can safely ignore any errors
306
+ }
307
+ }
308
+
245
309
  const action = req.query.action;
246
310
 
247
311
  if (action === 'signup' || action === 'signup-paid' || action === 'subscribe') {
@@ -322,5 +386,6 @@ module.exports = {
322
386
  updateMemberData,
323
387
  updateMemberNewsletters,
324
388
  deleteSession,
389
+ accessInfoSession,
325
390
  deleteSuppression
326
391
  };
@@ -33,12 +33,12 @@ const mongoTransformer = flowRight(statusTransformer, rejectNonStatusTransformer
33
33
 
34
34
  /**
35
35
  * @typedef {object} BaseOptions
36
- * @prop {import('knex').Transaction} transacting
36
+ * @prop {import('knex').Knex.Transaction} transacting
37
37
  */
38
38
 
39
39
  /**
40
40
  * @typedef {object} ListOptions
41
- * @prop {import('knex').Transaction} transacting
41
+ * @prop {import('knex').Knex.Transaction} transacting
42
42
  * @prop {string} filter
43
43
  */
44
44
 
@@ -56,7 +56,7 @@ class OfferBookshelfRepository {
56
56
 
57
57
  /**
58
58
  * @template T
59
- * @param {(t: import('knex').Transaction) => Promise<T>} cb
59
+ * @param {(t: import('knex').Knex.Transaction) => Promise<T>} cb
60
60
  * @returns {Promise<T>}
61
61
  */
62
62
  async createTransaction(cb) {
@@ -1,4 +1,5 @@
1
1
  <!DOCTYPE html>
2
+ <html>
2
3
  <head>
3
4
  <meta charset="utf-8">
4
5
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
@@ -45,7 +45,14 @@ module.exports = function setupMembersApp() {
45
45
  membersApp.put('/api/member/newsletters', bodyParser.json({limit: '50mb'}), middleware.updateMemberNewsletters);
46
46
 
47
47
  // Get and update member data
48
- membersApp.get('/api/member', middleware.getMemberData);
48
+ // Caching members content is an experimental feature
49
+ const shouldCacheMembersContent = config.get('cacheMembersContent:enabled');
50
+ if (shouldCacheMembersContent) {
51
+ membersApp.get('/api/member', middleware.loadMemberSession, middleware.accessInfoSession, middleware.getMemberData);
52
+ } else {
53
+ membersApp.get('/api/member', middleware.getMemberData);
54
+ }
55
+
49
56
  membersApp.put('/api/member', bodyParser.json({limit: '50mb'}), middleware.updateMemberData);
50
57
  membersApp.post('/api/member/email', bodyParser.json({limit: '50mb'}), (req, res) => membersService.api.middleware.updateEmailAddress(req, res));
51
58
 
@@ -115,7 +115,6 @@ if (sentryConfig && !sentryConfig.disabled) {
115
115
  release: 'ghost@' + version,
116
116
  environment: environment,
117
117
  maxValueLength: 1000,
118
- includeLocalVariables: true,
119
118
  integrations: [
120
119
  Sentry.extraErrorDataIntegration()
121
120
  ],