ghost 5.114.1 → 5.115.1

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 (201) hide show
  1. package/components/tryghost-adapter-cache-redis-5.115.1.tgz +0 -0
  2. package/components/{tryghost-adapter-manager-5.114.1.tgz → tryghost-adapter-manager-5.115.1.tgz} +0 -0
  3. package/components/{tryghost-announcement-bar-settings-5.114.1.tgz → tryghost-announcement-bar-settings-5.115.1.tgz} +0 -0
  4. package/components/{tryghost-api-framework-5.114.1.tgz → tryghost-api-framework-5.115.1.tgz} +0 -0
  5. package/components/tryghost-constants-5.115.1.tgz +0 -0
  6. package/components/tryghost-custom-fonts-5.115.1.tgz +0 -0
  7. package/components/{tryghost-custom-theme-settings-service-5.114.1.tgz → tryghost-custom-theme-settings-service-5.115.1.tgz} +0 -0
  8. package/components/{tryghost-data-generator-5.114.1.tgz → tryghost-data-generator-5.115.1.tgz} +0 -0
  9. package/components/{tryghost-domain-events-5.114.1.tgz → tryghost-domain-events-5.115.1.tgz} +0 -0
  10. package/components/tryghost-donations-5.115.1.tgz +0 -0
  11. package/components/tryghost-email-addresses-5.115.1.tgz +0 -0
  12. package/components/{tryghost-email-content-generator-5.114.1.tgz → tryghost-email-content-generator-5.115.1.tgz} +0 -0
  13. package/components/tryghost-email-events-5.115.1.tgz +0 -0
  14. package/components/tryghost-email-service-5.115.1.tgz +0 -0
  15. package/components/tryghost-email-suppression-list-5.115.1.tgz +0 -0
  16. package/components/tryghost-express-dynamic-redirects-5.115.1.tgz +0 -0
  17. package/components/tryghost-ghost-5.115.1.tgz +0 -0
  18. package/components/{tryghost-html-to-plaintext-5.114.1.tgz → tryghost-html-to-plaintext-5.115.1.tgz} +0 -0
  19. package/components/tryghost-i18n-5.115.1.tgz +0 -0
  20. package/components/tryghost-importer-handler-content-files-5.115.1.tgz +0 -0
  21. package/components/tryghost-in-memory-repository-5.115.1.tgz +0 -0
  22. package/components/{tryghost-job-manager-5.114.1.tgz → tryghost-job-manager-5.115.1.tgz} +0 -0
  23. package/components/{tryghost-link-redirects-5.114.1.tgz → tryghost-link-redirects-5.115.1.tgz} +0 -0
  24. package/components/tryghost-link-replacer-5.115.1.tgz +0 -0
  25. package/components/{tryghost-magic-link-5.114.1.tgz → tryghost-magic-link-5.115.1.tgz} +0 -0
  26. package/components/{tryghost-mailgun-client-5.114.1.tgz → tryghost-mailgun-client-5.115.1.tgz} +0 -0
  27. package/components/tryghost-member-attribution-5.115.1.tgz +0 -0
  28. package/components/{tryghost-member-events-5.114.1.tgz → tryghost-member-events-5.115.1.tgz} +0 -0
  29. package/components/{tryghost-members-api-5.114.1.tgz → tryghost-members-api-5.115.1.tgz} +0 -0
  30. package/components/{tryghost-members-csv-5.114.1.tgz → tryghost-members-csv-5.115.1.tgz} +0 -0
  31. package/components/{tryghost-members-offers-5.114.1.tgz → tryghost-members-offers-5.115.1.tgz} +0 -0
  32. package/components/{tryghost-members-payments-5.114.1.tgz → tryghost-members-payments-5.115.1.tgz} +0 -0
  33. package/components/{tryghost-milestones-5.114.1.tgz → tryghost-milestones-5.115.1.tgz} +0 -0
  34. package/components/tryghost-minifier-5.115.1.tgz +0 -0
  35. package/components/{tryghost-mw-error-handler-5.114.1.tgz → tryghost-mw-error-handler-5.115.1.tgz} +0 -0
  36. package/components/{tryghost-mw-version-match-5.114.1.tgz → tryghost-mw-version-match-5.115.1.tgz} +0 -0
  37. package/components/tryghost-mw-vhost-5.115.1.tgz +0 -0
  38. package/components/{tryghost-post-events-5.114.1.tgz → tryghost-post-events-5.115.1.tgz} +0 -0
  39. package/components/{tryghost-post-revisions-5.114.1.tgz → tryghost-post-revisions-5.115.1.tgz} +0 -0
  40. package/components/{tryghost-posts-service-5.114.1.tgz → tryghost-posts-service-5.115.1.tgz} +0 -0
  41. package/components/{tryghost-prometheus-metrics-5.114.1.tgz → tryghost-prometheus-metrics-5.115.1.tgz} +0 -0
  42. package/components/tryghost-recommendations-5.115.1.tgz +0 -0
  43. package/components/{tryghost-security-5.114.1.tgz → tryghost-security-5.115.1.tgz} +0 -0
  44. package/components/tryghost-slack-notifications-5.115.1.tgz +0 -0
  45. package/components/{tryghost-tiers-5.114.1.tgz → tryghost-tiers-5.115.1.tgz} +0 -0
  46. package/components/{tryghost-webmentions-5.114.1.tgz → tryghost-webmentions-5.115.1.tgz} +0 -0
  47. package/content/themes/casper/LICENSE +1 -1
  48. package/content/themes/casper/README.md +1 -1
  49. package/content/themes/source/LICENSE +1 -1
  50. package/content/themes/source/README.md +1 -1
  51. package/content/themes/source/assets/built/screen.css +1 -1
  52. package/content/themes/source/assets/built/screen.css.map +1 -1
  53. package/content/themes/source/assets/css/screen.css +11 -6
  54. package/content/themes/source/partials/feature-image.hbs +2 -2
  55. package/core/boot.js +3 -1
  56. package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +23497 -23041
  57. package/core/built/admin/assets/admin-x-demo/admin-x-demo.js +1 -1
  58. package/core/built/admin/assets/admin-x-demo/{index-0040480a.mjs → index-15df2af5.mjs} +4 -3
  59. package/core/built/admin/assets/admin-x-demo/{modals-fb35c86c.mjs → modals-8ca61d78.mjs} +67 -65
  60. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-806ef39c.mjs → CodeEditorView-d2e6872f.mjs} +2 -2
  61. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +1 -1
  62. package/core/built/admin/assets/admin-x-settings/{index-376f847c.mjs → index-8e8821e5.mjs} +2 -2
  63. package/core/built/admin/assets/admin-x-settings/{index-8fa19303.mjs → index-f5cb3db3.mjs} +3104 -3094
  64. package/core/built/admin/assets/admin-x-settings/{modals-36775d71.mjs → modals-e8ae4d46.mjs} +3 -3
  65. package/core/built/admin/assets/{chunk.524.85c5b32bd46b91c147b9.js → chunk.524.2439684964c164c598ab.js} +7 -7
  66. package/core/built/admin/assets/{chunk.582.449a129a8005f03574bd.js → chunk.582.bf5a2bbb2c4eb69ef1e7.js} +10 -10
  67. package/core/built/admin/assets/ghost-327b17ea23cb8c89bd7e6a51e18e8506.css +1 -0
  68. package/core/built/admin/assets/ghost-dark-f30a597ac19632a118939492591c531b.css +1 -0
  69. package/core/built/admin/assets/{ghost-c563138cc2c0767bf6eefc9a2587eaa4.js → ghost-df7b9558260aa27d18b195ee895b487d.js} +182 -160
  70. package/core/built/admin/assets/stats/stats.js +11824 -0
  71. package/core/built/admin/index.html +4 -4
  72. package/core/frontend/helpers/ghost_head.js +3 -1
  73. package/core/frontend/src/cards/css/cta.css +1 -1
  74. package/core/server/api/endpoints/slugs.js +6 -2
  75. package/core/server/data/importer/import-manager.js +2 -2
  76. package/core/server/data/importer/importers/importer-revue.js +128 -0
  77. package/core/server/data/importer/importers/json-to-html.js +107 -0
  78. package/core/server/data/migrations/utils/tables.js +2 -4
  79. package/core/server/data/migrations/versions/5.115/2025-03-24-07-19-27-add-identity-read-permission-to-administrators.js +6 -0
  80. package/core/server/data/schema/fixtures/fixtures.json +2 -1
  81. package/core/server/lib/bootstrap-socket.js +87 -0
  82. package/core/server/lib/package-json/index.js +1 -0
  83. package/core/server/lib/package-json/package-json.js +160 -0
  84. package/core/server/lib/package-json/parse.js +57 -0
  85. package/core/server/models/base/plugins/actions.js +44 -31
  86. package/core/server/models/base/plugins/generate-slug.js +6 -0
  87. package/core/server/notify.js +1 -1
  88. package/core/server/services/activitypub/ActivityPubService.ts +1 -1
  89. package/core/server/services/api-version-compatibility/APIVersionCompatibilityService.js +99 -0
  90. package/core/server/services/api-version-compatibility/VersionNotificationsDataService.js +80 -0
  91. package/core/server/services/api-version-compatibility/extract-api-key.js +57 -0
  92. package/core/server/services/api-version-compatibility/index.js +2 -2
  93. package/core/server/services/api-version-compatibility/mw-api-version-mismatch.js +31 -0
  94. package/core/server/services/audience-feedback/AudienceFeedbackController.js +85 -0
  95. package/core/server/services/audience-feedback/AudienceFeedbackService.js +34 -0
  96. package/core/server/services/audience-feedback/Feedback.js +35 -0
  97. package/core/server/services/audience-feedback/index.js +4 -2
  98. package/core/server/services/auth/session/emails/signin.js +168 -0
  99. package/core/server/services/auth/session/index.js +2 -2
  100. package/core/server/services/auth/session/session-from-token.js +69 -0
  101. package/core/server/services/auth/session/session-service.js +364 -0
  102. package/core/server/services/email-analytics/EmailAnalyticsProviderMailgun.js +62 -0
  103. package/core/server/services/email-analytics/EmailAnalyticsService.js +552 -0
  104. package/core/server/services/email-analytics/EmailAnalyticsServiceWrapper.js +3 -3
  105. package/core/server/services/email-analytics/EventProcessingResult.js +66 -0
  106. package/core/server/services/explore-ping/ExplorePingService.js +106 -0
  107. package/core/server/services/explore-ping/index.js +31 -0
  108. package/core/server/services/identity-tokens/IdentityTokenService.js +30 -0
  109. package/core/server/services/identity-tokens/IdentityTokenService.ts +28 -0
  110. package/core/server/services/identity-tokens/IdentityTokenServiceWrapper.js +1 -1
  111. package/core/server/services/invitations/accept.js +5 -2
  112. package/core/server/services/mail-events/BookshelfMailEventRepository.js +2 -2
  113. package/core/server/services/mail-events/InMemoryMailEventRepository.js +10 -0
  114. package/core/server/services/mail-events/InMemoryMailEventRepository.ts +8 -0
  115. package/core/server/services/mail-events/MailEvent.js +20 -0
  116. package/core/server/services/mail-events/MailEvent.ts +10 -0
  117. package/core/server/services/mail-events/MailEventRepository.js +2 -0
  118. package/core/server/services/mail-events/MailEventRepository.ts +5 -0
  119. package/core/server/services/mail-events/MailEventService.js +124 -0
  120. package/core/server/services/mail-events/MailEventService.ts +169 -0
  121. package/core/server/services/mail-events/index.js +1 -1
  122. package/core/server/services/mail-events/libraries.d.ts +2 -0
  123. package/core/server/services/members/CaptchaService.js +80 -0
  124. package/core/server/services/members/api.js +1 -1
  125. package/core/server/services/members/importer/MembersCSVImporter.js +464 -0
  126. package/core/server/services/members/importer/MembersCSVImporterStripeUtils.js +194 -0
  127. package/core/server/services/members/importer/email-template.js +182 -0
  128. package/core/server/services/members/importer/index.js +30 -0
  129. package/core/server/services/members/members-ssr.js +333 -0
  130. package/core/server/services/members/service.js +2 -2
  131. package/core/server/services/posts/stats/PostStats.js +13 -0
  132. package/core/server/services/route-settings/SettingsPathManager.js +47 -0
  133. package/core/server/services/route-settings/index.js +1 -1
  134. package/core/server/services/stripe/README.md +63 -0
  135. package/core/server/services/stripe/StripeAPI.js +931 -0
  136. package/core/server/services/stripe/StripeMigrations.js +613 -0
  137. package/core/server/services/stripe/StripeService.js +175 -0
  138. package/core/server/services/stripe/WebhookController.js +100 -0
  139. package/core/server/services/stripe/WebhookManager.js +175 -0
  140. package/core/server/services/stripe/events/StripeLiveDisabledEvent.js +23 -0
  141. package/core/server/services/stripe/events/StripeLiveEnabledEvent.js +23 -0
  142. package/core/server/services/stripe/events/index.js +4 -0
  143. package/core/server/services/stripe/service.js +1 -1
  144. package/core/server/services/stripe/services/webhook/CheckoutSessionEventService.js +255 -0
  145. package/core/server/services/stripe/services/webhook/InvoiceEventService.js +70 -0
  146. package/core/server/services/stripe/services/webhook/SubscriptionEventService.js +54 -0
  147. package/core/server/services/themes/loader.js +1 -1
  148. package/core/server/services/themes/to-json.js +1 -1
  149. package/core/server/web/api/endpoints/admin/routes.js +1 -0
  150. package/core/server/web/shared/middleware/cache-control.js +51 -0
  151. package/core/server/web/shared/middleware/index.js +1 -1
  152. package/core/server/web/well-known.js +1 -1
  153. package/core/shared/labs.js +3 -1
  154. package/core/shared/settings-cache/CacheManager.js +64 -6
  155. package/package.json +103 -134
  156. package/tsconfig.tsbuildinfo +1 -1
  157. package/yarn.lock +7 -93
  158. package/components/tryghost-adapter-cache-redis-5.114.1.tgz +0 -0
  159. package/components/tryghost-api-version-compatibility-service-5.114.1.tgz +0 -0
  160. package/components/tryghost-audience-feedback-5.114.1.tgz +0 -0
  161. package/components/tryghost-bookshelf-repository-5.114.1.tgz +0 -0
  162. package/components/tryghost-bootstrap-socket-5.114.1.tgz +0 -0
  163. package/components/tryghost-captcha-service-5.114.1.tgz +0 -0
  164. package/components/tryghost-constants-5.114.1.tgz +0 -0
  165. package/components/tryghost-custom-fonts-5.114.1.tgz +0 -0
  166. package/components/tryghost-donations-5.114.1.tgz +0 -0
  167. package/components/tryghost-email-addresses-5.114.1.tgz +0 -0
  168. package/components/tryghost-email-analytics-provider-mailgun-5.114.1.tgz +0 -0
  169. package/components/tryghost-email-analytics-service-5.114.1.tgz +0 -0
  170. package/components/tryghost-email-events-5.114.1.tgz +0 -0
  171. package/components/tryghost-email-service-5.114.1.tgz +0 -0
  172. package/components/tryghost-email-suppression-list-5.114.1.tgz +0 -0
  173. package/components/tryghost-express-dynamic-redirects-5.114.1.tgz +0 -0
  174. package/components/tryghost-extract-api-key-5.114.1.tgz +0 -0
  175. package/components/tryghost-ghost-5.114.1.tgz +0 -0
  176. package/components/tryghost-i18n-5.114.1.tgz +0 -0
  177. package/components/tryghost-identity-token-service-5.114.1.tgz +0 -0
  178. package/components/tryghost-importer-handler-content-files-5.114.1.tgz +0 -0
  179. package/components/tryghost-importer-revue-5.114.1.tgz +0 -0
  180. package/components/tryghost-in-memory-repository-5.114.1.tgz +0 -0
  181. package/components/tryghost-link-replacer-5.114.1.tgz +0 -0
  182. package/components/tryghost-mail-events-5.114.1.tgz +0 -0
  183. package/components/tryghost-member-attribution-5.114.1.tgz +0 -0
  184. package/components/tryghost-members-importer-5.114.1.tgz +0 -0
  185. package/components/tryghost-members-ssr-5.114.1.tgz +0 -0
  186. package/components/tryghost-members-stripe-service-5.114.1.tgz +0 -0
  187. package/components/tryghost-minifier-5.114.1.tgz +0 -0
  188. package/components/tryghost-mw-api-version-mismatch-5.114.1.tgz +0 -0
  189. package/components/tryghost-mw-cache-control-5.114.1.tgz +0 -0
  190. package/components/tryghost-mw-session-from-token-5.114.1.tgz +0 -0
  191. package/components/tryghost-mw-update-user-last-seen-5.114.1.tgz +0 -0
  192. package/components/tryghost-mw-vhost-5.114.1.tgz +0 -0
  193. package/components/tryghost-package-json-5.114.1.tgz +0 -0
  194. package/components/tryghost-recommendations-5.114.1.tgz +0 -0
  195. package/components/tryghost-referrers-5.114.1.tgz +0 -0
  196. package/components/tryghost-session-service-5.114.1.tgz +0 -0
  197. package/components/tryghost-settings-path-manager-5.114.1.tgz +0 -0
  198. package/components/tryghost-slack-notifications-5.114.1.tgz +0 -0
  199. package/components/tryghost-version-notifications-data-service-5.114.1.tgz +0 -0
  200. package/core/built/admin/assets/ghost-c2a7c4a1b76550c4219adb2ed4124ce0.css +0 -1
  201. package/core/built/admin/assets/ghost-dark-f91e4a479c6d38d94d5d1b14727871dc.css +0 -1
@@ -0,0 +1,66 @@
1
+ class EventProcessingResult {
2
+ /**
3
+ * @param {object} result
4
+ * @param {number} [result.delivered]
5
+ * @param {number} [result.opened]
6
+ * @param {number} [result.temporaryFailed]
7
+ * @param {number} [result.permanentFailed]
8
+ * @param {number} [result.unsubscribed]
9
+ * @param {number} [result.complained]
10
+ * @param {number} [result.unhandled]
11
+ * @param {number} [result.unprocessable]
12
+ * @param {number} [result.processingFailures]
13
+ * @param {string[]} [result.emailIds]
14
+ * @param {string[]} [result.memberIds]
15
+ */
16
+ constructor(result = {}) {
17
+ // counts
18
+ this.delivered = 0;
19
+ this.opened = 0;
20
+ this.temporaryFailed = 0;
21
+ this.permanentFailed = 0;
22
+ this.unsubscribed = 0;
23
+ this.complained = 0;
24
+ this.unhandled = 0;
25
+ this.unprocessable = 0;
26
+
27
+ // processing failures are counted separately in addition to event type counts
28
+ this.processingFailures = 0;
29
+
30
+ // ids seen whilst processing ready for passing to the stats aggregator
31
+ this.emailIds = [];
32
+ this.memberIds = [];
33
+
34
+ this.merge(result);
35
+ }
36
+
37
+ get totalEvents() {
38
+ return this.delivered
39
+ + this.opened
40
+ + this.temporaryFailed
41
+ + this.permanentFailed
42
+ + this.unsubscribed
43
+ + this.complained
44
+ + this.unhandled
45
+ + this.unprocessable;
46
+ }
47
+
48
+ merge(other = {}) {
49
+ this.delivered += other.delivered || 0;
50
+ this.opened += other.opened || 0;
51
+ this.temporaryFailed += other.temporaryFailed || 0;
52
+ this.permanentFailed += other.permanentFailed || 0;
53
+ this.unsubscribed += other.unsubscribed || 0;
54
+ this.complained += other.complained || 0;
55
+ this.unhandled += other.unhandled || 0;
56
+ this.unprocessable += other.unprocessable || 0;
57
+
58
+ this.processingFailures += other.processingFailures || 0;
59
+
60
+ // TODO: come up with a cleaner way to merge these without churning through Array and Set
61
+ this.emailIds = Array.from(new Set([...this.emailIds, ...(other.emailIds || [])])).filter(Boolean);
62
+ this.memberIds = Array.from(new Set([...this.memberIds, ...(other.memberIds || [])])).filter(Boolean);
63
+ }
64
+ }
65
+
66
+ module.exports = EventProcessingResult;
@@ -0,0 +1,106 @@
1
+ module.exports = class ExplorePingService {
2
+ /**
3
+ * @param {object} deps
4
+ * @param {{getPublic: () => import('../../../shared/settings-cache/CacheManager').PublicSettingsCache}} deps.settingsCache
5
+ * @param {object} deps.config
6
+ * @param {object} deps.labs
7
+ * @param {object} deps.logging
8
+ * @param {object} deps.ghostVersion
9
+ * @param {object} deps.request
10
+ * @param {{stats: {
11
+ * getMostRecentlyPublishedPostDate: () => Promise<Date>,
12
+ * getFirstPublishedPostDate: () => Promise<Date>,
13
+ * getTotalPostsPublished: () => Promise<number>
14
+ * }}} deps.posts
15
+ * @param {{stats: {
16
+ * getTotalMembers: () => Promise<number>
17
+ * }}} deps.members
18
+ */
19
+ constructor({settingsCache, config, labs, logging, ghostVersion, request, posts, members}) {
20
+ this.settingsCache = settingsCache;
21
+ this.config = config;
22
+ this.labs = labs;
23
+ this.logging = logging;
24
+ this.ghostVersion = ghostVersion;
25
+ this.request = request;
26
+ this.posts = posts;
27
+ this.members = members;
28
+ }
29
+
30
+ async constructPayload() {
31
+ /* eslint-disable camelcase */
32
+ const {title, description, icon, locale, accent_color, twitter, facebook} = this.settingsCache.getPublic();
33
+
34
+ // Get post statistics
35
+ const [totalPosts, lastPublishedAt, firstPublishedAt] = await Promise.all([
36
+ this.posts.stats.getTotalPostsPublished(),
37
+ this.posts.stats.getMostRecentlyPublishedPostDate(),
38
+ this.posts.stats.getFirstPublishedPostDate()
39
+ ]);
40
+
41
+ // Get member statistics with error handling
42
+ let totalMembers = null;
43
+ try {
44
+ totalMembers = await this.members.stats.getTotalMembers();
45
+ } catch (err) {
46
+ this.logging.warn('Failed to fetch member statistics', {
47
+ error: err.message,
48
+ context: 'explore-ping-service'
49
+ });
50
+ // Continue without member statistics
51
+ }
52
+
53
+ return {
54
+ ghost: this.ghostVersion.full,
55
+ url: this.config.get('url'),
56
+ title,
57
+ description,
58
+ icon,
59
+ locale,
60
+ accent_color,
61
+ twitter,
62
+ facebook,
63
+ posts_first: firstPublishedAt ? firstPublishedAt.toISOString() : null,
64
+ posts_last: lastPublishedAt ? lastPublishedAt.toISOString() : null,
65
+ posts_total: totalPosts,
66
+ members_total: totalMembers
67
+ };
68
+ /* eslint-enable camelcase */
69
+ }
70
+
71
+ async makeRequest(exploreUrl, payload) {
72
+ const json = JSON.stringify(payload);
73
+ this.logging.info('Pinging Explore with Payload', exploreUrl, json);
74
+
75
+ try {
76
+ const response = await this.request(exploreUrl, {
77
+ method: 'POST',
78
+ body: json,
79
+ headers: {
80
+ 'Content-Type': 'application/json'
81
+ }
82
+ });
83
+
84
+ this.logging.info('Explore Response', response.statusCode, response.statusMessage);
85
+
86
+ return response;
87
+ } catch (err) {
88
+ this.logging.warn('Explore Error', err.message);
89
+ }
90
+ }
91
+
92
+ async ping() {
93
+ if (!this.labs.isSet('explore')) {
94
+ return;
95
+ }
96
+
97
+ const exploreUrl = this.config.get('explore:url');
98
+ if (!exploreUrl) {
99
+ this.logging.warn('Explore URL not set');
100
+ return;
101
+ }
102
+
103
+ const payload = await this.constructPayload();
104
+ await this.makeRequest(exploreUrl, payload);
105
+ }
106
+ };
@@ -0,0 +1,31 @@
1
+ const ExplorePingService = require('./ExplorePingService');
2
+ const config = require('../../../shared/config');
3
+ const labs = require('../../../shared/labs');
4
+ const logging = require('@tryghost/logging');
5
+ const ghostVersion = require('@tryghost/version');
6
+ const request = require('@tryghost/request');
7
+ const settingsCache = require('../../../shared/settings-cache');
8
+ const posts = require('../posts/posts-service');
9
+ const members = require('../members');
10
+
11
+ // Export the creation function for testing
12
+ module.exports.createService = function createService() {
13
+ return new ExplorePingService({
14
+ settingsCache,
15
+ config,
16
+ labs,
17
+ logging,
18
+ ghostVersion,
19
+ request,
20
+ posts: posts(),
21
+ members
22
+ });
23
+ };
24
+
25
+ module.exports.init = async function init() {
26
+ const explorePingService = module.exports.createService();
27
+
28
+ // The final intention is to have this run on a schedule
29
+ // For the initial version, we'll just ping when the server starts
30
+ await explorePingService.ping();
31
+ };
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.IdentityTokenService = void 0;
4
+ const jsonwebtoken_1 = require("jsonwebtoken");
5
+ class IdentityTokenService {
6
+ privateKey;
7
+ issuer;
8
+ keyId;
9
+ constructor(privateKey, issuer, keyId) {
10
+ this.privateKey = privateKey;
11
+ this.issuer = issuer;
12
+ this.keyId = keyId;
13
+ }
14
+ async getTokenForUser(email, role) {
15
+ const claims = {
16
+ sub: email
17
+ };
18
+ if (typeof role === 'string') {
19
+ claims.role = role;
20
+ }
21
+ const token = (0, jsonwebtoken_1.sign)(claims, this.privateKey, {
22
+ issuer: this.issuer,
23
+ expiresIn: '5m',
24
+ algorithm: 'RS256',
25
+ keyid: this.keyId
26
+ });
27
+ return token;
28
+ }
29
+ }
30
+ exports.IdentityTokenService = IdentityTokenService;
@@ -0,0 +1,28 @@
1
+ import {sign} from 'jsonwebtoken';
2
+
3
+ export class IdentityTokenService {
4
+ constructor(
5
+ private privateKey: string,
6
+ private issuer: string,
7
+ private keyId: string
8
+ ) {}
9
+
10
+ async getTokenForUser(email: string, role?: string) {
11
+ const claims: Record<string, string> = {
12
+ sub: email
13
+ };
14
+
15
+ if (typeof role === 'string') {
16
+ claims.role = role;
17
+ }
18
+
19
+ const token = sign(claims, this.privateKey, {
20
+ issuer: this.issuer,
21
+ expiresIn: '5m',
22
+ algorithm: 'RS256',
23
+ keyid: this.keyId
24
+ });
25
+
26
+ return token;
27
+ }
28
+ }
@@ -1,4 +1,4 @@
1
- const {IdentityTokenService} = require('@tryghost/identity-token-service');
1
+ const {IdentityTokenService} = require('./IdentityTokenService');
2
2
 
3
3
  module.exports = class IdentityTokenServiceWrapper {
4
4
  /** @type IdentityTokenService */
@@ -2,13 +2,16 @@ const errors = require('@tryghost/errors');
2
2
  const tpl = require('@tryghost/tpl');
3
3
  const models = require('../../models');
4
4
  const security = require('@tryghost/security');
5
- const messages = {inviteNotFound: 'Invite not found.',
5
+
6
+ const messages = {
7
+ inviteNotFound: 'Invite not found.',
6
8
  inviteExpired: 'Invite is expired.',
7
9
  inviteEmailAlreadyExist: {
8
10
  message: 'Could not create an account, email is already in use.',
9
11
  context: 'Attempting to create an account with existing email address.',
10
12
  help: 'Use different email address to register your account.'
11
- }};
13
+ }
14
+ };
12
15
 
13
16
  async function accept(invitation) {
14
17
  const data = invitation.invitation[0];
@@ -1,6 +1,6 @@
1
1
  /**
2
- * @typedef {import('@tryghost/mail-events').MailEventRepository} MailEventRepository
3
- * @typedef {import('@tryghost/mail-events').MailEvent} MailEvent
2
+ * @typedef {import('./MailEventRepository')} MailEventRepository
3
+ * @typedef {import('./MailEvent').MailEvent} MailEvent
4
4
  */
5
5
 
6
6
  /**
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InMemoryMailEventRepository = void 0;
4
+ const in_memory_repository_1 = require("@tryghost/in-memory-repository");
5
+ class InMemoryMailEventRepository extends in_memory_repository_1.InMemoryRepository {
6
+ toPrimitive() {
7
+ return {};
8
+ }
9
+ }
10
+ exports.InMemoryMailEventRepository = InMemoryMailEventRepository;
@@ -0,0 +1,8 @@
1
+ import {InMemoryRepository} from '@tryghost/in-memory-repository';
2
+ import {MailEvent} from './MailEvent';
3
+
4
+ export class InMemoryMailEventRepository extends InMemoryRepository<string, MailEvent> {
5
+ protected toPrimitive(): object {
6
+ return {};
7
+ }
8
+ }
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MailEvent = void 0;
4
+ class MailEvent {
5
+ id;
6
+ type;
7
+ messageId;
8
+ recipient;
9
+ timestampMs;
10
+ deleted;
11
+ constructor(id, type, messageId, recipient, timestampMs, deleted = false) {
12
+ this.id = id;
13
+ this.type = type;
14
+ this.messageId = messageId;
15
+ this.recipient = recipient;
16
+ this.timestampMs = timestampMs;
17
+ this.deleted = deleted;
18
+ }
19
+ }
20
+ exports.MailEvent = MailEvent;
@@ -0,0 +1,10 @@
1
+ export class MailEvent {
2
+ constructor(
3
+ readonly id: string,
4
+ readonly type: string,
5
+ readonly messageId: string,
6
+ readonly recipient: string,
7
+ readonly timestampMs: number,
8
+ readonly deleted: boolean = false
9
+ ) {}
10
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,5 @@
1
+ import {MailEvent} from './MailEvent';
2
+
3
+ export interface MailEventRepository {
4
+ save(event: MailEvent): Promise<void>;
5
+ }
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.MailEventService = void 0;
7
+ const crypto_1 = __importDefault(require("crypto"));
8
+ const errors_1 = __importDefault(require("@tryghost/errors"));
9
+ const tpl_1 = __importDefault(require("@tryghost/tpl"));
10
+ const MailEvent_1 = require("./MailEvent");
11
+ /**
12
+ * @see https://documentation.mailgun.com/en/latest/user_manual.html#events-1
13
+ */
14
+ var EventType;
15
+ (function (EventType) {
16
+ EventType["CLICKED"] = "clicked";
17
+ EventType["COMPLAINED"] = "complained";
18
+ EventType["DELIVERED"] = "delivered";
19
+ EventType["FAILED"] = "failed";
20
+ EventType["OPENED"] = "opened";
21
+ EventType["UNSUBSCRIBED"] = "unsubscribed";
22
+ })(EventType || (EventType = {}));
23
+ const VALIDATION_MESSAGES = {
24
+ signingKeyNotConfigured: 'payload signing key is not configured',
25
+ payloadSignatureMissing: 'Payload is missing "signature"',
26
+ payloadSignatureInvalid: '"signature" is invalid',
27
+ payloadEventsMissing: 'Payload is missing "mail_events"',
28
+ payloadEventsInvalid: '"mail_events" is not an array',
29
+ payloadEventKeyMissing: 'Event [{idx}] is missing "{key}"'
30
+ };
31
+ class MailEventService {
32
+ mailEventRepository;
33
+ config;
34
+ labs;
35
+ static LABS_KEY = 'mailEvents';
36
+ static CONFIG_KEY_PAYLOAD_SIGNING_KEY = 'hostSettings:mailEventsPayloadSigningKey';
37
+ constructor(mailEventRepository, config, labs) {
38
+ this.mailEventRepository = mailEventRepository;
39
+ this.config = config;
40
+ this.labs = labs;
41
+ }
42
+ async processPayload(payload) {
43
+ if (this.labs.isSet(MailEventService.LABS_KEY) === false) {
44
+ throw new errors_1.default.NotFoundError();
45
+ }
46
+ const payloadSigningKey = this.config.get(MailEventService.CONFIG_KEY_PAYLOAD_SIGNING_KEY);
47
+ // Verify that the service is configured correctly - We expect a string
48
+ // for the payload signing key but as a safeguard we check the type here
49
+ // to prevent any unexpected behaviour
50
+ if (typeof payloadSigningKey !== 'string') {
51
+ throw new errors_1.default.InternalServerError({
52
+ message: (0, tpl_1.default)(VALIDATION_MESSAGES.signingKeyNotConfigured)
53
+ });
54
+ }
55
+ // Verify the payload
56
+ this.verifyPayload(payload, payloadSigningKey);
57
+ // Store known events
58
+ const eventTypes = new Set(Object.values(EventType));
59
+ for (const payloadEvent of payload.mail_events) {
60
+ if (eventTypes.has(payloadEvent.event) === false) {
61
+ continue;
62
+ }
63
+ try {
64
+ await this.mailEventRepository.save(new MailEvent_1.MailEvent(payloadEvent.id, payloadEvent.event, payloadEvent.message.headers['message-id'], payloadEvent.recipient, payloadEvent.timestamp * 1000));
65
+ }
66
+ catch (err) {
67
+ throw new errors_1.default.InternalServerError({
68
+ message: 'Event could not be stored',
69
+ err: err
70
+ });
71
+ }
72
+ }
73
+ }
74
+ validatePayload(payload) {
75
+ if (payload.signature === undefined) {
76
+ throw new errors_1.default.ValidationError({
77
+ message: (0, tpl_1.default)(VALIDATION_MESSAGES.payloadSignatureMissing)
78
+ });
79
+ }
80
+ if (typeof payload.signature !== 'string') {
81
+ throw new errors_1.default.ValidationError({
82
+ message: (0, tpl_1.default)(VALIDATION_MESSAGES.payloadSignatureInvalid)
83
+ });
84
+ }
85
+ if (payload.mail_events === undefined) {
86
+ throw new errors_1.default.ValidationError({
87
+ message: (0, tpl_1.default)(VALIDATION_MESSAGES.payloadEventsMissing)
88
+ });
89
+ }
90
+ if (Array.isArray(payload.mail_events) === false) {
91
+ throw new errors_1.default.ValidationError({
92
+ message: (0, tpl_1.default)(VALIDATION_MESSAGES.payloadEventsInvalid)
93
+ });
94
+ }
95
+ const expectedKeys = ['id', 'timestamp', 'event', 'message', 'recipient'];
96
+ payload.mail_events.forEach((payloadEvent, idx) => {
97
+ expectedKeys.forEach((key) => {
98
+ if (payloadEvent[key] === undefined) {
99
+ throw new errors_1.default.ValidationError({
100
+ message: (0, tpl_1.default)(VALIDATION_MESSAGES.payloadEventKeyMissing, { idx, key })
101
+ });
102
+ }
103
+ if (key === 'message' && payloadEvent.message.headers?.['message-id'] === undefined) {
104
+ throw new errors_1.default.ValidationError({
105
+ message: (0, tpl_1.default)(VALIDATION_MESSAGES.payloadEventKeyMissing, { idx, key: 'message.headers.message-id' })
106
+ });
107
+ }
108
+ });
109
+ });
110
+ }
111
+ verifyPayload(payload, payloadSigningKey) {
112
+ const data = JSON.stringify(payload.mail_events);
113
+ const signature = crypto_1.default
114
+ .createHmac('sha256', payloadSigningKey)
115
+ .update(data)
116
+ .digest('hex');
117
+ if (signature !== payload.signature) {
118
+ throw new errors_1.default.UnauthorizedError({
119
+ message: (0, tpl_1.default)(VALIDATION_MESSAGES.payloadSignatureInvalid)
120
+ });
121
+ }
122
+ }
123
+ }
124
+ exports.MailEventService = MailEventService;
@@ -0,0 +1,169 @@
1
+ import crypto from 'crypto';
2
+ import errors from '@tryghost/errors';
3
+ import tpl from '@tryghost/tpl';
4
+
5
+ import {MailEvent} from './MailEvent';
6
+ import {MailEventRepository} from './MailEventRepository';
7
+
8
+ /**
9
+ * @see https://documentation.mailgun.com/en/latest/user_manual.html#events-1
10
+ */
11
+ enum EventType { // eslint-disable-line no-shadow
12
+ CLICKED = 'clicked',
13
+ COMPLAINED = 'complained',
14
+ DELIVERED = 'delivered',
15
+ FAILED = 'failed',
16
+ OPENED = 'opened',
17
+ UNSUBSCRIBED = 'unsubscribed'
18
+ }
19
+
20
+ interface PayloadEvent {
21
+ id: string;
22
+ timestamp: number; // Unix timestamp in seconds
23
+ event: string;
24
+ message: {
25
+ headers: {
26
+ 'message-id': string;
27
+ }
28
+ },
29
+ recipient: string;
30
+ }
31
+
32
+ interface Payload {
33
+ signature: string;
34
+ mail_events: PayloadEvent[];
35
+ }
36
+
37
+ interface Labs {
38
+ isSet(key: string): boolean;
39
+ }
40
+
41
+ interface Config {
42
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
43
+ get(key: string): any;
44
+ }
45
+
46
+ const VALIDATION_MESSAGES = {
47
+ signingKeyNotConfigured: 'payload signing key is not configured',
48
+ payloadSignatureMissing: 'Payload is missing "signature"',
49
+ payloadSignatureInvalid: '"signature" is invalid',
50
+ payloadEventsMissing: 'Payload is missing "mail_events"',
51
+ payloadEventsInvalid: '"mail_events" is not an array',
52
+ payloadEventKeyMissing: 'Event [{idx}] is missing "{key}"'
53
+ };
54
+
55
+ export class MailEventService {
56
+ static readonly LABS_KEY = 'mailEvents';
57
+ static readonly CONFIG_KEY_PAYLOAD_SIGNING_KEY = 'hostSettings:mailEventsPayloadSigningKey';
58
+
59
+ constructor(
60
+ private mailEventRepository: MailEventRepository,
61
+ private config: Config,
62
+ private labs: Labs
63
+ ) {}
64
+
65
+ async processPayload(payload: Payload) {
66
+ if (this.labs.isSet(MailEventService.LABS_KEY) === false) {
67
+ throw new errors.NotFoundError();
68
+ }
69
+
70
+ const payloadSigningKey = this.config.get(MailEventService.CONFIG_KEY_PAYLOAD_SIGNING_KEY);
71
+
72
+ // Verify that the service is configured correctly - We expect a string
73
+ // for the payload signing key but as a safeguard we check the type here
74
+ // to prevent any unexpected behaviour
75
+ if (typeof payloadSigningKey !== 'string') {
76
+ throw new errors.InternalServerError({
77
+ message: tpl(VALIDATION_MESSAGES.signingKeyNotConfigured)
78
+ });
79
+ }
80
+
81
+ // Verify the payload
82
+ this.verifyPayload(payload, payloadSigningKey);
83
+
84
+ // Store known events
85
+ const eventTypes = new Set<string>(Object.values(EventType) as string[]);
86
+
87
+ for (const payloadEvent of payload.mail_events) {
88
+ if (eventTypes.has(payloadEvent.event) === false) {
89
+ continue;
90
+ }
91
+
92
+ try {
93
+ await this.mailEventRepository.save(
94
+ new MailEvent(
95
+ payloadEvent.id,
96
+ payloadEvent.event,
97
+ payloadEvent.message.headers['message-id'],
98
+ payloadEvent.recipient,
99
+ payloadEvent.timestamp * 1000
100
+ )
101
+ );
102
+ } catch (err) {
103
+ throw new errors.InternalServerError({
104
+ message: 'Event could not be stored',
105
+ err: err
106
+ });
107
+ }
108
+ }
109
+ }
110
+
111
+ validatePayload(payload: Payload) {
112
+ if (payload.signature === undefined) {
113
+ throw new errors.ValidationError({
114
+ message: tpl(VALIDATION_MESSAGES.payloadSignatureMissing)
115
+ });
116
+ }
117
+
118
+ if (typeof payload.signature !== 'string') {
119
+ throw new errors.ValidationError({
120
+ message: tpl(VALIDATION_MESSAGES.payloadSignatureInvalid)
121
+ });
122
+ }
123
+
124
+ if (payload.mail_events === undefined) {
125
+ throw new errors.ValidationError({
126
+ message: tpl(VALIDATION_MESSAGES.payloadEventsMissing)
127
+ });
128
+ }
129
+
130
+ if (Array.isArray(payload.mail_events) === false) {
131
+ throw new errors.ValidationError({
132
+ message: tpl(VALIDATION_MESSAGES.payloadEventsInvalid)
133
+ });
134
+ }
135
+
136
+ const expectedKeys: (keyof PayloadEvent)[] = ['id', 'timestamp', 'event', 'message', 'recipient'];
137
+
138
+ payload.mail_events.forEach((payloadEvent, idx) => {
139
+ expectedKeys.forEach((key) => {
140
+ if (payloadEvent[key] === undefined) {
141
+ throw new errors.ValidationError({
142
+ message: tpl(VALIDATION_MESSAGES.payloadEventKeyMissing, {idx, key})
143
+ });
144
+ }
145
+
146
+ if (key === 'message' && payloadEvent.message.headers?.['message-id'] === undefined) {
147
+ throw new errors.ValidationError({
148
+ message: tpl(VALIDATION_MESSAGES.payloadEventKeyMissing, {idx, key: 'message.headers.message-id'})
149
+ });
150
+ }
151
+ });
152
+ });
153
+ }
154
+
155
+ private verifyPayload(payload: Payload, payloadSigningKey: string) {
156
+ const data = JSON.stringify(payload.mail_events);
157
+
158
+ const signature = crypto
159
+ .createHmac('sha256', payloadSigningKey)
160
+ .update(data)
161
+ .digest('hex');
162
+
163
+ if (signature !== payload.signature) {
164
+ throw new errors.UnauthorizedError({
165
+ message: tpl(VALIDATION_MESSAGES.payloadSignatureInvalid)
166
+ });
167
+ }
168
+ }
169
+ }
@@ -1,4 +1,4 @@
1
- const {MailEventService} = require('@tryghost/mail-events');
1
+ const {MailEventService} = require('./MailEventService');
2
2
  const MailEventRepository = require('./BookshelfMailEventRepository.js');
3
3
 
4
4
  class MailEventsServiceWrapper {
@@ -0,0 +1,2 @@
1
+ declare module '@tryghost/errors';
2
+ declare module '@tryghost/tpl';