ghost 5.115.0 → 5.116.0

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 (341) hide show
  1. package/components/{tryghost-api-framework-5.115.0.tgz → tryghost-api-framework-5.116.0.tgz} +0 -0
  2. package/components/tryghost-constants-5.116.0.tgz +0 -0
  3. package/components/tryghost-custom-fonts-5.116.0.tgz +0 -0
  4. package/components/{tryghost-custom-theme-settings-service-5.115.0.tgz → tryghost-custom-theme-settings-service-5.116.0.tgz} +0 -0
  5. package/components/tryghost-domain-events-5.116.0.tgz +0 -0
  6. package/components/{tryghost-donations-5.115.0.tgz → tryghost-donations-5.116.0.tgz} +0 -0
  7. package/components/tryghost-email-addresses-5.116.0.tgz +0 -0
  8. package/components/tryghost-email-service-5.116.0.tgz +0 -0
  9. package/components/tryghost-email-suppression-list-5.116.0.tgz +0 -0
  10. package/components/tryghost-html-to-plaintext-5.116.0.tgz +0 -0
  11. package/components/tryghost-i18n-5.116.0.tgz +0 -0
  12. package/components/tryghost-job-manager-5.116.0.tgz +0 -0
  13. package/components/tryghost-link-replacer-5.116.0.tgz +0 -0
  14. package/components/tryghost-magic-link-5.116.0.tgz +0 -0
  15. package/components/tryghost-member-attribution-5.116.0.tgz +0 -0
  16. package/components/tryghost-member-events-5.116.0.tgz +0 -0
  17. package/components/tryghost-members-api-5.116.0.tgz +0 -0
  18. package/components/tryghost-members-csv-5.116.0.tgz +0 -0
  19. package/components/{tryghost-members-offers-5.115.0.tgz → tryghost-members-offers-5.116.0.tgz} +0 -0
  20. package/components/{tryghost-milestones-5.115.0.tgz → tryghost-milestones-5.116.0.tgz} +0 -0
  21. package/components/{tryghost-mw-error-handler-5.115.0.tgz → tryghost-mw-error-handler-5.116.0.tgz} +0 -0
  22. package/components/tryghost-mw-vhost-5.116.0.tgz +0 -0
  23. package/components/{tryghost-post-events-5.115.0.tgz → tryghost-post-events-5.116.0.tgz} +0 -0
  24. package/components/{tryghost-post-revisions-5.115.0.tgz → tryghost-post-revisions-5.116.0.tgz} +0 -0
  25. package/components/tryghost-posts-service-5.116.0.tgz +0 -0
  26. package/components/{tryghost-prometheus-metrics-5.115.0.tgz → tryghost-prometheus-metrics-5.116.0.tgz} +0 -0
  27. package/components/tryghost-security-5.116.0.tgz +0 -0
  28. package/components/{tryghost-tiers-5.115.0.tgz → tryghost-tiers-5.116.0.tgz} +0 -0
  29. package/components/tryghost-webmentions-5.116.0.tgz +0 -0
  30. package/content/themes/casper/LICENSE +1 -1
  31. package/content/themes/casper/README.md +1 -1
  32. package/content/themes/source/LICENSE +1 -1
  33. package/content/themes/source/README.md +1 -1
  34. package/content/themes/source/assets/built/screen.css +1 -1
  35. package/content/themes/source/assets/built/screen.css.map +1 -1
  36. package/content/themes/source/assets/css/screen.css +11 -6
  37. package/content/themes/source/partials/feature-image.hbs +2 -2
  38. package/core/boot.js +3 -43
  39. package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +30494 -29403
  40. package/core/built/admin/assets/admin-x-demo/admin-x-demo.js +1 -1
  41. package/core/built/admin/assets/admin-x-demo/{index-0040480a.mjs → index-a9601514.mjs} +5 -4
  42. package/core/built/admin/assets/admin-x-demo/{modals-fb35c86c.mjs → modals-c1789d04.mjs} +67 -65
  43. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-806ef39c.mjs → CodeEditorView-e9c9deb8.mjs} +2 -2
  44. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +2 -2
  45. package/core/built/admin/assets/admin-x-settings/{index-376f847c.mjs → index-84580c3a.mjs} +2 -2
  46. package/core/built/admin/assets/admin-x-settings/{index-8fa19303.mjs → index-f744cab7.mjs} +3147 -3123
  47. package/core/built/admin/assets/admin-x-settings/{modals-36775d71.mjs → modals-d9ca60c5.mjs} +1198 -1192
  48. package/core/built/admin/assets/chunk.524.8371443ef8f60db429d0.js +35 -0
  49. package/core/built/admin/assets/chunk.582.f90151775f2e53dd21d9.js +37 -0
  50. package/core/built/admin/assets/{chunk.874.461cb3cf5b6b36915f8c.js → chunk.713.e9027c0cc3c56110f5da.js} +125 -98
  51. package/core/built/admin/assets/{ghost-938b3d9c29e3564a53a22f8c8f82d351.js → ghost-03b64c086f3c60cabc85fe7a7e2b640a.js} +272 -251
  52. package/core/built/admin/assets/ghost-ba58e9822f7384461e926c7e23f04a75.css +1 -0
  53. package/core/built/admin/assets/ghost-dark-f1f29683b14ffa11615b3bba8b6ab92c.css +1 -0
  54. package/core/built/admin/assets/koenig-lexical/index.css +1 -1
  55. package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +20563 -20891
  56. package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +139 -139
  57. package/core/built/admin/assets/posts/posts.js +5732 -5667
  58. package/core/built/admin/assets/stats/stats.js +75373 -0
  59. package/core/built/admin/assets/{vendor-68a4aa424a179a90f5bbc2b750def576.js → vendor-72026232b36d97babc6320917c16c321.js} +36 -34
  60. package/core/built/admin/index.html +6 -6
  61. package/core/cli/generate-data.js +1 -1
  62. package/core/frontend/helpers/ghost_head.js +8 -1
  63. package/core/frontend/public/ghost-stats.js +55 -2
  64. package/core/frontend/services/assets-minification/AdminAuthAssets.js +2 -1
  65. package/core/frontend/services/assets-minification/CardAssets.js +1 -1
  66. package/core/frontend/services/assets-minification/CommentCountsAssets.js +1 -1
  67. package/core/frontend/services/assets-minification/MemberAttributionAssets.js +1 -1
  68. package/core/frontend/services/assets-minification/Minifier.js +191 -0
  69. package/core/frontend/services/routing/controllers/previews.js +2 -1
  70. package/core/frontend/src/cards/css/cta.css +1 -1
  71. package/core/server/adapters/cache/Redis.js +1 -1
  72. package/core/server/adapters/lib/redis/AdapterCacheRedis.js +287 -0
  73. package/core/server/adapters/lib/redis/redis-store-factory.js +22 -0
  74. package/core/server/api/endpoints/posts.js +9 -3
  75. package/core/server/api/endpoints/previews.js +35 -1
  76. package/core/server/api/endpoints/slugs.js +6 -2
  77. package/core/server/api/endpoints/utils/serializers/output/utils/post-gating.js +6 -9
  78. package/core/server/api/endpoints/utils/validators/input/settings.js +1 -1
  79. package/core/server/data/db/connection.js +2 -0
  80. package/core/server/data/db/index.js +1 -0
  81. package/core/server/data/importer/handlers/ImporterContentFileHandler.js +90 -0
  82. package/core/server/data/importer/import-manager.js +3 -3
  83. package/core/server/data/importer/importers/importer-revue.js +128 -0
  84. package/core/server/data/importer/importers/json-to-html.js +107 -0
  85. package/core/server/data/migrations/utils/tables.js +2 -4
  86. package/core/server/data/seeders/DataGenerator.js +288 -0
  87. package/core/server/data/seeders/importers/BenefitsImporter.js +28 -0
  88. package/core/server/data/seeders/importers/CommentsImporter.js +73 -0
  89. package/core/server/data/seeders/importers/EmailBatchesImporter.js +38 -0
  90. package/core/server/data/seeders/importers/EmailRecipientFailuresImporter.js +67 -0
  91. package/core/server/data/seeders/importers/EmailRecipientsImporter.js +212 -0
  92. package/core/server/data/seeders/importers/EmailsImporter.js +99 -0
  93. package/core/server/data/seeders/importers/LabelsImporter.js +41 -0
  94. package/core/server/data/seeders/importers/MembersClickEventsImporter.js +69 -0
  95. package/core/server/data/seeders/importers/MembersCreatedEventsImporter.js +103 -0
  96. package/core/server/data/seeders/importers/MembersFeedbackImporter.js +45 -0
  97. package/core/server/data/seeders/importers/MembersImporter.js +111 -0
  98. package/core/server/data/seeders/importers/MembersLabelsImporter.js +39 -0
  99. package/core/server/data/seeders/importers/MembersLoginEventsImporter.js +69 -0
  100. package/core/server/data/seeders/importers/MembersNewslettersImporter.js +38 -0
  101. package/core/server/data/seeders/importers/MembersPaidSubscriptionEventsImporter.js +99 -0
  102. package/core/server/data/seeders/importers/MembersProductsImporter.js +42 -0
  103. package/core/server/data/seeders/importers/MembersStatusEventsImporter.js +58 -0
  104. package/core/server/data/seeders/importers/MembersStripeCustomersImporter.js +60 -0
  105. package/core/server/data/seeders/importers/MembersStripeCustomersSubscriptionsImporter.js +259 -0
  106. package/core/server/data/seeders/importers/MembersSubscribeEventsImporter.js +69 -0
  107. package/core/server/data/seeders/importers/MembersSubscriptionCreatedEventsImporter.js +95 -0
  108. package/core/server/data/seeders/importers/NewslettersImporter.js +40 -0
  109. package/core/server/data/seeders/importers/OffersImporter.js +70 -0
  110. package/core/server/data/seeders/importers/PostsAuthorsImporter.js +32 -0
  111. package/core/server/data/seeders/importers/PostsImporter.js +102 -0
  112. package/core/server/data/seeders/importers/PostsProductsImporter.js +35 -0
  113. package/core/server/data/seeders/importers/PostsTagsImporter.js +46 -0
  114. package/core/server/data/seeders/importers/ProductsBenefitsImporter.js +54 -0
  115. package/core/server/data/seeders/importers/ProductsImporter.js +90 -0
  116. package/core/server/data/seeders/importers/RecommendationClickEventsImporter.js +32 -0
  117. package/core/server/data/seeders/importers/RecommendationSubscribeEventsImporter.js +32 -0
  118. package/core/server/data/seeders/importers/RecommendationsImporter.js +34 -0
  119. package/core/server/data/seeders/importers/RedirectsImporter.js +49 -0
  120. package/core/server/data/seeders/importers/RolesUsersImporter.js +42 -0
  121. package/core/server/data/seeders/importers/StripePricesImporter.js +69 -0
  122. package/core/server/data/seeders/importers/StripeProductsImporter.js +34 -0
  123. package/core/server/data/seeders/importers/TableImporter.js +187 -0
  124. package/core/server/data/seeders/importers/TagsImporter.js +41 -0
  125. package/core/server/data/seeders/importers/UsersImporter.js +31 -0
  126. package/core/server/data/seeders/importers/WebMentionsImporter.js +42 -0
  127. package/core/server/data/seeders/importers/index.js +41 -0
  128. package/core/server/data/seeders/utils/JsonImporter.js +39 -0
  129. package/core/server/data/seeders/utils/blog-info.js +3 -0
  130. package/core/server/data/seeders/utils/database-date.js +7 -0
  131. package/core/server/data/seeders/utils/event-generator.js +48 -0
  132. package/core/server/data/seeders/utils/random.js +13 -0
  133. package/core/server/data/seeders/utils/topological-sort.js +33 -0
  134. package/core/server/lib/bootstrap-socket.js +87 -0
  135. package/core/server/lib/package-json/index.js +1 -0
  136. package/core/server/lib/package-json/package-json.js +160 -0
  137. package/core/server/lib/package-json/parse.js +57 -0
  138. package/core/server/models/base/plugins/actions.js +44 -31
  139. package/core/server/models/base/plugins/generate-slug.js +6 -0
  140. package/core/server/notify.js +1 -1
  141. package/core/server/services/activitypub/ActivityPubService.ts +1 -1
  142. package/core/server/services/adapter-manager/AdapterManager.js +161 -0
  143. package/core/server/services/adapter-manager/index.js +1 -1
  144. package/core/server/services/announcement-bar-service/AnnouncementBarSettings.js +54 -0
  145. package/core/server/services/announcement-bar-service/AnnouncementVisibilityValues.js +11 -0
  146. package/core/server/services/announcement-bar-service/index.js +1 -1
  147. package/core/server/services/api-version-compatibility/APIVersionCompatibilityService.js +99 -0
  148. package/core/server/services/api-version-compatibility/VersionNotificationsDataService.js +80 -0
  149. package/core/server/services/api-version-compatibility/extract-api-key.js +57 -0
  150. package/core/server/services/api-version-compatibility/index.js +2 -2
  151. package/core/server/services/api-version-compatibility/mw-api-version-mismatch.js +31 -0
  152. package/core/server/services/audience-feedback/AudienceFeedbackController.js +85 -0
  153. package/core/server/services/audience-feedback/AudienceFeedbackService.js +34 -0
  154. package/core/server/services/audience-feedback/Feedback.js +35 -0
  155. package/core/server/services/audience-feedback/index.js +4 -2
  156. package/core/server/services/auth/session/emails/signin.js +168 -0
  157. package/core/server/services/auth/session/index.js +2 -2
  158. package/core/server/services/auth/session/session-from-token.js +69 -0
  159. package/core/server/services/auth/session/session-service.js +374 -0
  160. package/core/server/services/custom-redirects/index.js +1 -1
  161. package/core/server/services/email-analytics/EmailAnalyticsProviderMailgun.js +62 -0
  162. package/core/server/services/email-analytics/EmailAnalyticsService.js +552 -0
  163. package/core/server/services/email-analytics/EmailAnalyticsServiceWrapper.js +3 -3
  164. package/core/server/services/email-analytics/EventProcessingResult.js +66 -0
  165. package/core/server/services/email-service/EmailServiceWrapper.js +4 -4
  166. package/core/server/services/email-suppression-list/MailgunEmailSuppressionList.js +1 -1
  167. package/core/server/services/email-suppression-list/service.js +1 -1
  168. package/core/server/services/explore-ping/ExplorePingService.js +106 -0
  169. package/core/server/services/explore-ping/index.js +31 -0
  170. package/core/server/services/identity-tokens/IdentityTokenService.js +30 -0
  171. package/core/server/services/identity-tokens/IdentityTokenService.ts +28 -0
  172. package/core/server/services/identity-tokens/IdentityTokenServiceWrapper.js +1 -1
  173. package/core/server/services/invitations/accept.js +5 -2
  174. package/core/server/services/lib/DynamicRedirectManager.js +156 -0
  175. package/core/server/services/lib/EmailContentGenerator.js +54 -0
  176. package/core/server/services/lib/InMemoryRepository.js +62 -0
  177. package/core/server/services/lib/InMemoryRepository.ts +80 -0
  178. package/core/server/services/lib/MailgunClient.js +364 -0
  179. package/core/server/services/link-redirection/LinkRedirect.js +26 -0
  180. package/core/server/services/link-redirection/LinkRedirectRepository.js +7 -7
  181. package/core/server/services/link-redirection/LinkRedirectsService.js +123 -0
  182. package/core/server/services/link-redirection/README.md +151 -0
  183. package/core/server/services/link-redirection/RedirectEvent.js +24 -0
  184. package/core/server/services/link-redirection/index.js +1 -1
  185. package/core/server/services/link-tracking/LinkClickTrackingService.js +1 -1
  186. package/core/server/services/mail/index.js +1 -1
  187. package/core/server/services/mail-events/BookshelfMailEventRepository.js +2 -2
  188. package/core/server/services/mail-events/InMemoryMailEventRepository.js +10 -0
  189. package/core/server/services/mail-events/InMemoryMailEventRepository.ts +8 -0
  190. package/core/server/services/mail-events/MailEvent.js +20 -0
  191. package/core/server/services/mail-events/MailEvent.ts +10 -0
  192. package/core/server/services/mail-events/MailEventRepository.js +2 -0
  193. package/core/server/services/mail-events/MailEventRepository.ts +5 -0
  194. package/core/server/services/mail-events/MailEventService.js +124 -0
  195. package/core/server/services/mail-events/MailEventService.ts +169 -0
  196. package/core/server/services/mail-events/index.js +1 -1
  197. package/core/server/services/mail-events/libraries.d.ts +2 -0
  198. package/core/server/services/members/CaptchaService.js +80 -0
  199. package/core/server/services/members/api.js +1 -1
  200. package/core/server/services/members/importer/MembersCSVImporter.js +464 -0
  201. package/core/server/services/members/importer/MembersCSVImporterStripeUtils.js +194 -0
  202. package/core/server/services/members/importer/email-template.js +182 -0
  203. package/core/server/services/members/importer/index.js +30 -0
  204. package/core/server/services/members/members-ssr.js +333 -0
  205. package/core/server/services/members/service.js +2 -2
  206. package/core/server/services/members-events/LastSeenAtUpdater.js +1 -1
  207. package/core/server/services/offers/service.js +1 -1
  208. package/core/server/services/posts/stats/PostStats.js +13 -0
  209. package/core/server/services/recommendations/RecommendationServiceWrapper.js +8 -8
  210. package/core/server/services/recommendations/service/BookshelfClickEventRepository.js +48 -0
  211. package/core/server/services/recommendations/service/BookshelfClickEventRepository.ts +49 -0
  212. package/core/server/services/recommendations/service/BookshelfRecommendationRepository.js +98 -0
  213. package/core/server/services/recommendations/service/BookshelfRecommendationRepository.ts +117 -0
  214. package/core/server/services/recommendations/service/BookshelfRepository.js +134 -0
  215. package/core/server/services/recommendations/service/BookshelfRepository.ts +196 -0
  216. package/core/server/services/recommendations/service/BookshelfSubscribeEventRepository.js +48 -0
  217. package/core/server/services/recommendations/service/BookshelfSubscribeEventRepository.ts +49 -0
  218. package/core/server/services/recommendations/service/ClickEvent.js +33 -0
  219. package/core/server/services/recommendations/service/ClickEvent.ts +32 -0
  220. package/core/server/services/recommendations/service/InMemoryRecommendationRepository.js +19 -0
  221. package/core/server/services/recommendations/service/InMemoryRecommendationRepository.ts +20 -0
  222. package/core/server/services/recommendations/service/IncomingRecommendationController.js +34 -0
  223. package/core/server/services/recommendations/service/IncomingRecommendationController.ts +51 -0
  224. package/core/server/services/recommendations/service/IncomingRecommendationEmailRenderer.js +25 -0
  225. package/core/server/services/recommendations/service/IncomingRecommendationEmailRenderer.ts +37 -0
  226. package/core/server/services/recommendations/service/IncomingRecommendationService.js +93 -0
  227. package/core/server/services/recommendations/service/IncomingRecommendationService.ts +160 -0
  228. package/core/server/services/recommendations/service/Recommendation.js +140 -0
  229. package/core/server/services/recommendations/service/Recommendation.ts +201 -0
  230. package/core/server/services/recommendations/service/RecommendationController.js +208 -0
  231. package/core/server/services/recommendations/service/RecommendationController.ts +258 -0
  232. package/core/server/services/recommendations/service/RecommendationMetadataService.js +86 -0
  233. package/core/server/services/recommendations/service/RecommendationMetadataService.ts +128 -0
  234. package/core/server/services/recommendations/service/RecommendationRepository.js +2 -0
  235. package/core/server/services/recommendations/service/RecommendationRepository.ts +13 -0
  236. package/core/server/services/recommendations/service/RecommendationService.js +228 -0
  237. package/core/server/services/recommendations/service/RecommendationService.ts +281 -0
  238. package/core/server/services/recommendations/service/SubscribeEvent.js +33 -0
  239. package/core/server/services/recommendations/service/SubscribeEvent.ts +32 -0
  240. package/core/server/services/recommendations/service/UnsafeData.js +183 -0
  241. package/core/server/services/recommendations/service/UnsafeData.ts +217 -0
  242. package/core/server/services/recommendations/service/WellknownService.js +36 -0
  243. package/core/server/services/recommendations/service/WellknownService.ts +47 -0
  244. package/core/server/services/recommendations/service/index.js +31 -0
  245. package/core/server/services/recommendations/service/index.ts +15 -0
  246. package/core/server/services/recommendations/service/libraries.d.ts +5 -0
  247. package/core/server/services/route-settings/SettingsPathManager.js +47 -0
  248. package/core/server/services/route-settings/index.js +1 -1
  249. package/core/server/services/slack-notifications/SlackNotifications.js +211 -0
  250. package/core/server/services/slack-notifications/SlackNotificationsService.js +90 -0
  251. package/core/server/services/slack-notifications/service.js +4 -6
  252. package/core/server/services/stripe/README.md +63 -0
  253. package/core/server/services/stripe/StripeAPI.js +931 -0
  254. package/core/server/services/stripe/StripeMigrations.js +613 -0
  255. package/core/server/services/stripe/StripeService.js +175 -0
  256. package/core/server/services/stripe/WebhookController.js +100 -0
  257. package/core/server/services/stripe/WebhookManager.js +175 -0
  258. package/core/server/services/stripe/events/StripeLiveDisabledEvent.js +23 -0
  259. package/core/server/services/stripe/events/StripeLiveEnabledEvent.js +23 -0
  260. package/core/server/services/stripe/events/index.js +4 -0
  261. package/core/server/services/stripe/service.js +1 -1
  262. package/core/server/services/stripe/services/webhook/CheckoutSessionEventService.js +255 -0
  263. package/core/server/services/stripe/services/webhook/InvoiceEventService.js +70 -0
  264. package/core/server/services/stripe/services/webhook/SubscriptionEventService.js +54 -0
  265. package/core/server/services/themes/loader.js +1 -1
  266. package/core/server/services/themes/to-json.js +1 -1
  267. package/core/server/web/api/endpoints/admin/app.js +1 -21
  268. package/core/server/web/api/endpoints/admin/routes.js +1 -0
  269. package/core/server/web/api/middleware/version-match.js +41 -0
  270. package/core/server/web/shared/middleware/cache-control.js +51 -0
  271. package/core/server/web/shared/middleware/index.js +1 -1
  272. package/core/server/web/well-known.js +1 -1
  273. package/core/shared/labs.js +5 -3
  274. package/core/shared/settings-cache/CacheManager.js +64 -6
  275. package/package.json +98 -146
  276. package/tsconfig.tsbuildinfo +1 -1
  277. package/yarn.lock +1478 -1634
  278. package/components/tryghost-adapter-cache-redis-5.115.0.tgz +0 -0
  279. package/components/tryghost-adapter-manager-5.115.0.tgz +0 -0
  280. package/components/tryghost-announcement-bar-settings-5.115.0.tgz +0 -0
  281. package/components/tryghost-api-version-compatibility-service-5.115.0.tgz +0 -0
  282. package/components/tryghost-audience-feedback-5.115.0.tgz +0 -0
  283. package/components/tryghost-bookshelf-repository-5.115.0.tgz +0 -0
  284. package/components/tryghost-bootstrap-socket-5.115.0.tgz +0 -0
  285. package/components/tryghost-captcha-service-5.115.0.tgz +0 -0
  286. package/components/tryghost-constants-5.115.0.tgz +0 -0
  287. package/components/tryghost-custom-fonts-5.115.0.tgz +0 -0
  288. package/components/tryghost-data-generator-5.115.0.tgz +0 -0
  289. package/components/tryghost-domain-events-5.115.0.tgz +0 -0
  290. package/components/tryghost-email-addresses-5.115.0.tgz +0 -0
  291. package/components/tryghost-email-analytics-provider-mailgun-5.115.0.tgz +0 -0
  292. package/components/tryghost-email-analytics-service-5.115.0.tgz +0 -0
  293. package/components/tryghost-email-content-generator-5.115.0.tgz +0 -0
  294. package/components/tryghost-email-events-5.115.0.tgz +0 -0
  295. package/components/tryghost-email-service-5.115.0.tgz +0 -0
  296. package/components/tryghost-email-suppression-list-5.115.0.tgz +0 -0
  297. package/components/tryghost-express-dynamic-redirects-5.115.0.tgz +0 -0
  298. package/components/tryghost-extract-api-key-5.115.0.tgz +0 -0
  299. package/components/tryghost-ghost-5.115.0.tgz +0 -0
  300. package/components/tryghost-html-to-plaintext-5.115.0.tgz +0 -0
  301. package/components/tryghost-i18n-5.115.0.tgz +0 -0
  302. package/components/tryghost-identity-token-service-5.115.0.tgz +0 -0
  303. package/components/tryghost-importer-handler-content-files-5.115.0.tgz +0 -0
  304. package/components/tryghost-importer-revue-5.115.0.tgz +0 -0
  305. package/components/tryghost-in-memory-repository-5.115.0.tgz +0 -0
  306. package/components/tryghost-job-manager-5.115.0.tgz +0 -0
  307. package/components/tryghost-link-redirects-5.115.0.tgz +0 -0
  308. package/components/tryghost-link-replacer-5.115.0.tgz +0 -0
  309. package/components/tryghost-magic-link-5.115.0.tgz +0 -0
  310. package/components/tryghost-mail-events-5.115.0.tgz +0 -0
  311. package/components/tryghost-mailgun-client-5.115.0.tgz +0 -0
  312. package/components/tryghost-member-attribution-5.115.0.tgz +0 -0
  313. package/components/tryghost-member-events-5.115.0.tgz +0 -0
  314. package/components/tryghost-members-api-5.115.0.tgz +0 -0
  315. package/components/tryghost-members-csv-5.115.0.tgz +0 -0
  316. package/components/tryghost-members-importer-5.115.0.tgz +0 -0
  317. package/components/tryghost-members-payments-5.115.0.tgz +0 -0
  318. package/components/tryghost-members-ssr-5.115.0.tgz +0 -0
  319. package/components/tryghost-members-stripe-service-5.115.0.tgz +0 -0
  320. package/components/tryghost-minifier-5.115.0.tgz +0 -0
  321. package/components/tryghost-mw-api-version-mismatch-5.115.0.tgz +0 -0
  322. package/components/tryghost-mw-cache-control-5.115.0.tgz +0 -0
  323. package/components/tryghost-mw-session-from-token-5.115.0.tgz +0 -0
  324. package/components/tryghost-mw-update-user-last-seen-5.115.0.tgz +0 -0
  325. package/components/tryghost-mw-version-match-5.115.0.tgz +0 -0
  326. package/components/tryghost-mw-vhost-5.115.0.tgz +0 -0
  327. package/components/tryghost-package-json-5.115.0.tgz +0 -0
  328. package/components/tryghost-posts-service-5.115.0.tgz +0 -0
  329. package/components/tryghost-recommendations-5.115.0.tgz +0 -0
  330. package/components/tryghost-referrers-5.115.0.tgz +0 -0
  331. package/components/tryghost-security-5.115.0.tgz +0 -0
  332. package/components/tryghost-session-service-5.115.0.tgz +0 -0
  333. package/components/tryghost-settings-path-manager-5.115.0.tgz +0 -0
  334. package/components/tryghost-slack-notifications-5.115.0.tgz +0 -0
  335. package/components/tryghost-version-notifications-data-service-5.115.0.tgz +0 -0
  336. package/components/tryghost-webmentions-5.115.0.tgz +0 -0
  337. package/core/built/admin/assets/chunk.524.31419fdf6fb3859ecc1e.js +0 -35
  338. package/core/built/admin/assets/chunk.582.08c816d5e4ab766486a7.js +0 -37
  339. package/core/built/admin/assets/ghost-c2a7c4a1b76550c4219adb2ed4124ce0.css +0 -1
  340. package/core/built/admin/assets/ghost-dark-f91e4a479c6d38d94d5d1b14727871dc.css +0 -1
  341. /package/core/built/admin/assets/{chunk.874.461cb3cf5b6b36915f8c.js.LICENSE.txt → chunk.713.e9027c0cc3c56110f5da.js.LICENSE.txt} +0 -0
@@ -0,0 +1,151 @@
1
+ # Link Redirects
2
+
3
+
4
+ ## Usage
5
+
6
+
7
+ ## Develop
8
+
9
+ This is a monorepo package.
10
+
11
+ Follow the instructions for the top-level repo.
12
+ 1. `git clone` this repo & `cd` into it as usual
13
+ 2. Run `yarn` to install top-level dependencies.
14
+
15
+
16
+
17
+ ## Test
18
+
19
+ - `yarn lint` run just eslint
20
+ - `yarn test` run lint and tests
21
+
22
+ ## Overview of how Ghost handles LinkRedirects
23
+ ### Summary
24
+ When a publisher sends an email newsletter with email click analytics enabled, Ghost will replace all the links in the email's content with a link of the form `https://{site_url}/r/{redirect hash}?m={member UUID}`. When a member clicks on a link in their email, Ghost receives the request, redirects the user to the original link's URL, then updates some analytics data in the database.
25
+
26
+ ### The details
27
+ The following deep-dive covers the link redirect flow from when the member clicks on a link in an email.
28
+
29
+ First, we lookup the redirect by the `/r/{hash}` value in the URL:
30
+ ```
31
+ select `redirects`.* from `redirects` where `redirects`.`from` = ? limit ? undefined
32
+ ```
33
+
34
+ If the redirect exists, the `LinkRedirectsService` emits a `RedirectEvent`, and then responds to the HTTP request with a 302.
35
+
36
+ The `LinkClickTrackingService` subscribes to the `RedirectEvent` and kicks off the analytics inserts/updates. First we grab the `uuid` from the `?m={uuid}` parameter and lookup the member by `uuid`:
37
+ ```
38
+ select `members`.* from `members` where `members`.`uuid` = ? limit ? undefined
39
+ ```
40
+
41
+ Then we insert a row into the `members_click_events` table to record the click:
42
+ ```
43
+ insert into `members_click_events` (`created_at`, `id`, `member_id`, `redirect_id`) values (?, ?, ?, ?) undefined
44
+ ```
45
+
46
+ Then we query for the row we just inserted:
47
+ ```
48
+ select `members_click_events`.* from `members_click_events` where `members_click_events`.`id` = ? limit ? undefined
49
+ ```
50
+
51
+ At this point, we emit a `MemberLinkClickEvent` with the member ID and `lastSeenAt` timestamp.
52
+
53
+ The `LastSeenAtUpdater` subscribes to the `MemberLinkClickEvent`. First, it checks if the `lastSeenAt` value has already been updated in the current day.
54
+
55
+ If it has, we stop here.
56
+
57
+ If it hasn't, we continue to update the member. First, we select the member by ID:
58
+ ```
59
+ select `members`.* from `members` where `members`.`id` = ? limit ? undefined
60
+ ```
61
+
62
+ Then we start a transaction and get a lock on the member for updating:
63
+ ```
64
+ BEGIN; trx34
65
+ select `members`.* from `members` where `members`.`id` = ? limit ? for update trx34
66
+ ```
67
+
68
+ Since we're editing the member, we will eventually need to emit a `member.edited` webhook with the standard includes (labels and newsletters) so we also query them here:
69
+ ```
70
+ select `labels`.*, `members_labels`.`member_id` as `_pivot_member_id`, `members_labels`.`label_id` as `_pivot_label_id`, `members_labels`.`sort_order` as `_pivot_sort_order` from `labels` inner join `members_labels` on `members_labels`.`label_id` = `labels`.`id` where `members_labels`.`member_id` in (?) order by `sort_order` ASC for update trx34
71
+ ```
72
+
73
+ Then we query the member's newsletters:
74
+ ```
75
+ select `newsletters`.*, `members_newsletters`.`member_id` as `_pivot_member_id`, `members_newsletters`.`newsletter_id` as `_pivot_newsletter_id` from `newsletters` inner join `members_newsletters` on `members_newsletters`.`newsletter_id` = `newsletters`.`id` where `members_newsletters`.`member_id` in (?) order by `newsletters`.`sort_order` ASC for update trx34
76
+ ```
77
+
78
+ Then we update the member:
79
+ ```
80
+ update `members` set `uuid` = ?, `transient_id` = ?, `email` = ?, `status` = ?, `name` = ?, `expertise` = ?, `note` = ?, `geolocation` = ?, `enable_comment_notifications` = ?, `email_count` = ?, `email_opened_count` = ?, `email_open_rate` = ?, `email_disabled` = ?, `last_seen_at` = ?, `last_commented_at` = ?, `created_at` = ?, `created_by` = ?, `updated_at` = ?, `updated_by` = ? where `id` = ? trx34
81
+ ```
82
+
83
+ Then we select the member by ID again to get the freshly updated values from the DB:
84
+ ```
85
+ select `members`.* from `members` where `members`.`id` = ? limit ? trx34
86
+ ```
87
+
88
+ Then we commit the transaction:
89
+ ```
90
+ COMMIT; trx34
91
+ ```
92
+
93
+ Finally, we query for any member.edited webhooks and fire the `member.edited` event:
94
+ ```
95
+ select `webhooks`.* from `webhooks` where `event` = ? trx34
96
+ ```
97
+
98
+
99
+ ### Sequence Diagram
100
+ ```mermaid
101
+ sequenceDiagram
102
+ actor Member
103
+ participant Ghost
104
+ participant Ghost Async
105
+ participant DB
106
+ rect rgba(0,100,0, 0.1)
107
+ Member ->>Ghost: Clicks link in email
108
+ Ghost ->> DB: Query: lookup redirect
109
+ DB ->> Ghost: Redirect record
110
+ Note right of DB: Serve the redirect
111
+ Ghost -->> Ghost Async: Emit RedirectEvent
112
+ Ghost ->> Member: 302 Redirect
113
+ end
114
+ rect rgba(100,0,0,0.1)
115
+ Ghost Async ->> DB: Lookup Member by `uuid` from URL param
116
+ DB ->> Ghost Async: `member` record
117
+ Ghost Async ->> DB: Insert `member_click_event`
118
+ Note right of DB: Insert click event
119
+ DB ->> Ghost Async: 👌
120
+ Ghost Async ->> DB: Select `member_click_event`
121
+ DB ->> Ghost Async: `member_click_event` record
122
+ end
123
+ rect rgba(0,0,100, 0.1)
124
+ Ghost Async ->> DB: Select `member` by id
125
+ DB ->> Ghost Async: `member` record
126
+ Ghost Async ->> DB: Begin transaction
127
+ activate DB
128
+ DB ->> Ghost Async: 👌
129
+ Ghost Async ->> DB: Select `member` for update
130
+ DB ->> Ghost Async: `member` record
131
+ Ghost Async ->> DB: Select member labels for update
132
+ Note right of DB: Update member `lastSeenAt`
133
+ DB ->> Ghost Async: Member's labels
134
+ Ghost Async ->> DB: Select member newsletters for update
135
+ DB ->> Ghost Async: Member's newsletters
136
+ Ghost Async ->> DB: Update member's `lastSeenAt` timestamp
137
+ DB ->> Ghost Async: 👌
138
+ Ghost Async ->> DB: Select `member` by ID
139
+ DB ->> Ghost Async: `member` record
140
+ Ghost Async ->> DB: Commit transaction
141
+ DB ->> Ghost Async: 👌
142
+ deactivate DB
143
+ end
144
+ rect rgba(100,100,0,0.1)
145
+ Ghost Async ->> DB: Select `webhooks`
146
+ Note right of DB: Send `member.edited` webhook
147
+ DB ->> Ghost Async: `webhook` records
148
+ create participant Webhook Recipient
149
+ Ghost Async ->> Webhook Recipient: `member.edited` webhook
150
+ end
151
+ ```
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @typedef {object} RedirectEventData
3
+ * @prop {URL} url
4
+ * @prop {import('./LinkRedirect')} link
5
+ */
6
+
7
+ module.exports = class RedirectEvent {
8
+ /**
9
+ * @param {RedirectEventData} data
10
+ * @param {Date} timestamp
11
+ */
12
+ constructor(data, timestamp) {
13
+ this.data = data;
14
+ this.timestamp = timestamp;
15
+ }
16
+
17
+ /**
18
+ * @param {RedirectEventData} data
19
+ * @param {Date} [timestamp]
20
+ */
21
+ static create(data, timestamp) {
22
+ return new RedirectEvent(data, timestamp ?? new Date);
23
+ }
24
+ };
@@ -14,7 +14,7 @@ class LinkRedirectsServiceWrapper {
14
14
  // Wire up all the dependencies
15
15
  const models = require('../../models');
16
16
 
17
- const {LinkRedirectsService} = require('@tryghost/link-redirects');
17
+ const LinkRedirectsService = require('./LinkRedirectsService');
18
18
 
19
19
  this.linkRedirectRepository = new LinkRedirectRepository({
20
20
  LinkRedirect: models.Redirect,
@@ -1,4 +1,4 @@
1
- const {RedirectEvent} = require('@tryghost/link-redirects');
1
+ const RedirectEvent = require('../link-redirection/RedirectEvent');
2
2
  const LinkClick = require('./ClickEvent');
3
3
  const PostLink = require('./PostLink');
4
4
  const ObjectID = require('bson-objectid').default;
@@ -1,7 +1,7 @@
1
1
  const path = require('path');
2
2
  const urlUtils = require('../../../shared/url-utils');
3
3
  const settingsCache = require('../../../shared/settings-cache');
4
- const EmailContentGenerator = require('@tryghost/email-content-generator');
4
+ const EmailContentGenerator = require('../lib/EmailContentGenerator');
5
5
 
6
6
  const emailContentGenerator = new EmailContentGenerator({
7
7
  getSiteUrl: () => urlUtils.urlFor('home', true),
@@ -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 InMemoryRepository_1 = require("../lib/InMemoryRepository");
5
+ class InMemoryMailEventRepository extends InMemoryRepository_1.InMemoryRepository {
6
+ toPrimitive() {
7
+ return {};
8
+ }
9
+ }
10
+ exports.InMemoryMailEventRepository = InMemoryMailEventRepository;
@@ -0,0 +1,8 @@
1
+ import {InMemoryRepository} from '../lib/InMemoryRepository';
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';
@@ -0,0 +1,80 @@
1
+ const hcaptcha = require('hcaptcha');
2
+ const logging = require('@tryghost/logging');
3
+ const {InternalServerError, BadRequestError, utils: errorUtils} = require('@tryghost/errors');
4
+
5
+ class CaptchaService {
6
+ #enabled;
7
+ #scoreThreshold;
8
+ #secretKey;
9
+
10
+ /**
11
+ * @param {Object} options
12
+ * @param {boolean} [options.enabled] Whether hCaptcha is enabled
13
+ * @param {number} [options.scoreThreshold] Score threshold for bot detection
14
+ * @param {string} [options.secretKey] hCaptcha secret key
15
+ */
16
+ constructor({
17
+ enabled,
18
+ scoreThreshold,
19
+ secretKey
20
+ }) {
21
+ this.#enabled = enabled;
22
+ this.#secretKey = secretKey;
23
+ this.#scoreThreshold = scoreThreshold;
24
+ }
25
+
26
+ getMiddleware() {
27
+ const scoreThreshold = this.#scoreThreshold;
28
+ const secretKey = this.#secretKey;
29
+
30
+ if (!this.#enabled) {
31
+ return function captchaNoOpMiddleware(req, res, next) {
32
+ next();
33
+ };
34
+ }
35
+
36
+ return async function captchaMiddleware(req, res, next) {
37
+ let captchaResponse;
38
+
39
+ try {
40
+ if (!req.body || !req.body.token) {
41
+ throw new BadRequestError({
42
+ message: 'hCaptcha token missing'
43
+ });
44
+ }
45
+
46
+ captchaResponse = await hcaptcha.verify(secretKey, req.body.token, req.ip);
47
+
48
+ if ('score' in captchaResponse && captchaResponse.score < scoreThreshold) {
49
+ // Using hCaptcha enterprise, so score is present
50
+ next();
51
+ } else if (!('score' in captchaResponse) && captchaResponse.success) {
52
+ // Using regular hCaptcha, so challenge-based
53
+ next();
54
+ } else {
55
+ logging.error(`Blocking request due to high score (${captchaResponse.score})`);
56
+
57
+ // Intentionally left sparse to avoid leaking information
58
+ throw new InternalServerError();
59
+ }
60
+ } catch (err) {
61
+ if (errorUtils.isGhostError(err)) {
62
+ return next(err);
63
+ } else {
64
+ const message = 'Failed to verify hCaptcha token';
65
+
66
+ logging.error(new InternalServerError({
67
+ message,
68
+ err
69
+ }));
70
+
71
+ return next(new InternalServerError({
72
+ message
73
+ }));
74
+ }
75
+ }
76
+ };
77
+ }
78
+ }
79
+
80
+ module.exports = CaptchaService;
@@ -18,7 +18,7 @@ const tiersService = require('../tiers');
18
18
  const newslettersService = require('../newsletters');
19
19
  const memberAttributionService = require('../member-attribution');
20
20
  const emailSuppressionList = require('../email-suppression-list');
21
- const CaptchaService = require('@tryghost/captcha-service');
21
+ const CaptchaService = require('./CaptchaService');
22
22
  const {t} = require('../i18n');
23
23
  const sentry = require('../../../shared/sentry');
24
24
  const sharedConfig = require('../../../shared/config');