ghost 5.115.1 → 5.116.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 (243) hide show
  1. package/components/{tryghost-api-framework-5.115.1.tgz → tryghost-api-framework-5.116.1.tgz} +0 -0
  2. package/components/tryghost-constants-5.116.1.tgz +0 -0
  3. package/components/tryghost-custom-fonts-5.116.1.tgz +0 -0
  4. package/components/{tryghost-custom-theme-settings-service-5.115.1.tgz → tryghost-custom-theme-settings-service-5.116.1.tgz} +0 -0
  5. package/components/{tryghost-domain-events-5.115.1.tgz → tryghost-domain-events-5.116.1.tgz} +0 -0
  6. package/components/{tryghost-donations-5.115.1.tgz → tryghost-donations-5.116.1.tgz} +0 -0
  7. package/components/tryghost-email-addresses-5.116.1.tgz +0 -0
  8. package/components/tryghost-email-service-5.116.1.tgz +0 -0
  9. package/components/tryghost-email-suppression-list-5.116.1.tgz +0 -0
  10. package/components/tryghost-html-to-plaintext-5.116.1.tgz +0 -0
  11. package/components/tryghost-i18n-5.116.1.tgz +0 -0
  12. package/components/tryghost-job-manager-5.116.1.tgz +0 -0
  13. package/components/tryghost-link-replacer-5.116.1.tgz +0 -0
  14. package/components/tryghost-magic-link-5.116.1.tgz +0 -0
  15. package/components/{tryghost-member-attribution-5.115.1.tgz → tryghost-member-attribution-5.116.1.tgz} +0 -0
  16. package/components/tryghost-member-events-5.116.1.tgz +0 -0
  17. package/components/tryghost-members-api-5.116.1.tgz +0 -0
  18. package/components/tryghost-members-csv-5.116.1.tgz +0 -0
  19. package/components/{tryghost-members-offers-5.115.1.tgz → tryghost-members-offers-5.116.1.tgz} +0 -0
  20. package/components/{tryghost-milestones-5.115.1.tgz → tryghost-milestones-5.116.1.tgz} +0 -0
  21. package/components/{tryghost-mw-error-handler-5.115.1.tgz → tryghost-mw-error-handler-5.116.1.tgz} +0 -0
  22. package/components/tryghost-mw-vhost-5.116.1.tgz +0 -0
  23. package/components/{tryghost-post-events-5.115.1.tgz → tryghost-post-events-5.116.1.tgz} +0 -0
  24. package/components/{tryghost-post-revisions-5.115.1.tgz → tryghost-post-revisions-5.116.1.tgz} +0 -0
  25. package/components/tryghost-posts-service-5.116.1.tgz +0 -0
  26. package/components/{tryghost-prometheus-metrics-5.115.1.tgz → tryghost-prometheus-metrics-5.116.1.tgz} +0 -0
  27. package/components/tryghost-security-5.116.1.tgz +0 -0
  28. package/components/{tryghost-tiers-5.115.1.tgz → tryghost-tiers-5.116.1.tgz} +0 -0
  29. package/components/tryghost-webmentions-5.116.1.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 +6 -11
  37. package/content/themes/source/partials/feature-image.hbs +2 -2
  38. package/core/boot.js +0 -42
  39. package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +24764 -24129
  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-15df2af5.mjs → index-a9601514.mjs} +3 -3
  42. package/core/built/admin/assets/admin-x-demo/{modals-8ca61d78.mjs → modals-c1789d04.mjs} +2 -2
  43. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-d2e6872f.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-8e8821e5.mjs → index-84580c3a.mjs} +2 -2
  46. package/core/built/admin/assets/admin-x-settings/{index-f5cb3db3.mjs → index-f744cab7.mjs} +49 -35
  47. package/core/built/admin/assets/admin-x-settings/{modals-e8ae4d46.mjs → modals-d9ca60c5.mjs} +1198 -1192
  48. package/core/built/admin/assets/chunk.524.cb72a86e19c9ffd6172e.js +35 -0
  49. package/core/built/admin/assets/chunk.582.4f4d38ffe79fbdbd26f7.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-df7b9558260aa27d18b195ee895b487d.js → ghost-03b64c086f3c60cabc85fe7a7e2b640a.js} +144 -145
  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 +71082 -7533
  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 +6 -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/server/adapters/cache/Redis.js +1 -1
  71. package/core/server/adapters/lib/redis/AdapterCacheRedis.js +287 -0
  72. package/core/server/adapters/lib/redis/redis-store-factory.js +22 -0
  73. package/core/server/api/endpoints/posts.js +9 -3
  74. package/core/server/api/endpoints/previews.js +35 -1
  75. package/core/server/api/endpoints/utils/serializers/output/utils/post-gating.js +6 -9
  76. package/core/server/api/endpoints/utils/validators/input/settings.js +1 -1
  77. package/core/server/data/db/connection.js +2 -0
  78. package/core/server/data/db/index.js +1 -0
  79. package/core/server/data/importer/handlers/ImporterContentFileHandler.js +90 -0
  80. package/core/server/data/importer/import-manager.js +1 -1
  81. package/core/server/data/seeders/DataGenerator.js +288 -0
  82. package/core/server/data/seeders/importers/BenefitsImporter.js +28 -0
  83. package/core/server/data/seeders/importers/CommentsImporter.js +73 -0
  84. package/core/server/data/seeders/importers/EmailBatchesImporter.js +38 -0
  85. package/core/server/data/seeders/importers/EmailRecipientFailuresImporter.js +67 -0
  86. package/core/server/data/seeders/importers/EmailRecipientsImporter.js +212 -0
  87. package/core/server/data/seeders/importers/EmailsImporter.js +99 -0
  88. package/core/server/data/seeders/importers/LabelsImporter.js +41 -0
  89. package/core/server/data/seeders/importers/MembersClickEventsImporter.js +69 -0
  90. package/core/server/data/seeders/importers/MembersCreatedEventsImporter.js +103 -0
  91. package/core/server/data/seeders/importers/MembersFeedbackImporter.js +45 -0
  92. package/core/server/data/seeders/importers/MembersImporter.js +111 -0
  93. package/core/server/data/seeders/importers/MembersLabelsImporter.js +39 -0
  94. package/core/server/data/seeders/importers/MembersLoginEventsImporter.js +69 -0
  95. package/core/server/data/seeders/importers/MembersNewslettersImporter.js +38 -0
  96. package/core/server/data/seeders/importers/MembersPaidSubscriptionEventsImporter.js +99 -0
  97. package/core/server/data/seeders/importers/MembersProductsImporter.js +42 -0
  98. package/core/server/data/seeders/importers/MembersStatusEventsImporter.js +58 -0
  99. package/core/server/data/seeders/importers/MembersStripeCustomersImporter.js +60 -0
  100. package/core/server/data/seeders/importers/MembersStripeCustomersSubscriptionsImporter.js +259 -0
  101. package/core/server/data/seeders/importers/MembersSubscribeEventsImporter.js +69 -0
  102. package/core/server/data/seeders/importers/MembersSubscriptionCreatedEventsImporter.js +95 -0
  103. package/core/server/data/seeders/importers/NewslettersImporter.js +40 -0
  104. package/core/server/data/seeders/importers/OffersImporter.js +70 -0
  105. package/core/server/data/seeders/importers/PostsAuthorsImporter.js +32 -0
  106. package/core/server/data/seeders/importers/PostsImporter.js +102 -0
  107. package/core/server/data/seeders/importers/PostsProductsImporter.js +35 -0
  108. package/core/server/data/seeders/importers/PostsTagsImporter.js +46 -0
  109. package/core/server/data/seeders/importers/ProductsBenefitsImporter.js +54 -0
  110. package/core/server/data/seeders/importers/ProductsImporter.js +90 -0
  111. package/core/server/data/seeders/importers/RecommendationClickEventsImporter.js +32 -0
  112. package/core/server/data/seeders/importers/RecommendationSubscribeEventsImporter.js +32 -0
  113. package/core/server/data/seeders/importers/RecommendationsImporter.js +34 -0
  114. package/core/server/data/seeders/importers/RedirectsImporter.js +49 -0
  115. package/core/server/data/seeders/importers/RolesUsersImporter.js +42 -0
  116. package/core/server/data/seeders/importers/StripePricesImporter.js +69 -0
  117. package/core/server/data/seeders/importers/StripeProductsImporter.js +34 -0
  118. package/core/server/data/seeders/importers/TableImporter.js +187 -0
  119. package/core/server/data/seeders/importers/TagsImporter.js +41 -0
  120. package/core/server/data/seeders/importers/UsersImporter.js +31 -0
  121. package/core/server/data/seeders/importers/WebMentionsImporter.js +42 -0
  122. package/core/server/data/seeders/importers/index.js +41 -0
  123. package/core/server/data/seeders/utils/JsonImporter.js +39 -0
  124. package/core/server/data/seeders/utils/blog-info.js +3 -0
  125. package/core/server/data/seeders/utils/database-date.js +7 -0
  126. package/core/server/data/seeders/utils/event-generator.js +48 -0
  127. package/core/server/data/seeders/utils/random.js +13 -0
  128. package/core/server/data/seeders/utils/topological-sort.js +33 -0
  129. package/core/server/services/adapter-manager/AdapterManager.js +161 -0
  130. package/core/server/services/adapter-manager/index.js +1 -1
  131. package/core/server/services/announcement-bar-service/AnnouncementBarSettings.js +54 -0
  132. package/core/server/services/announcement-bar-service/AnnouncementVisibilityValues.js +11 -0
  133. package/core/server/services/announcement-bar-service/index.js +1 -1
  134. package/core/server/services/api-version-compatibility/APIVersionCompatibilityService.js +1 -1
  135. package/core/server/services/auth/session/session-service.js +15 -5
  136. package/core/server/services/custom-redirects/index.js +1 -1
  137. package/core/server/services/email-analytics/EmailAnalyticsProviderMailgun.js +1 -1
  138. package/core/server/services/email-service/EmailServiceWrapper.js +4 -4
  139. package/core/server/services/email-suppression-list/MailgunEmailSuppressionList.js +1 -1
  140. package/core/server/services/email-suppression-list/service.js +1 -1
  141. package/core/server/services/lib/DynamicRedirectManager.js +156 -0
  142. package/core/server/services/lib/EmailContentGenerator.js +54 -0
  143. package/core/server/services/lib/InMemoryRepository.js +62 -0
  144. package/core/server/services/lib/InMemoryRepository.ts +80 -0
  145. package/core/server/services/lib/MailgunClient.js +364 -0
  146. package/core/server/services/link-redirection/LinkRedirect.js +26 -0
  147. package/core/server/services/link-redirection/LinkRedirectRepository.js +7 -7
  148. package/core/server/services/link-redirection/LinkRedirectsService.js +123 -0
  149. package/core/server/services/link-redirection/README.md +151 -0
  150. package/core/server/services/link-redirection/RedirectEvent.js +24 -0
  151. package/core/server/services/link-redirection/index.js +1 -1
  152. package/core/server/services/link-tracking/LinkClickTrackingService.js +1 -1
  153. package/core/server/services/mail/index.js +1 -1
  154. package/core/server/services/mail-events/InMemoryMailEventRepository.js +2 -2
  155. package/core/server/services/mail-events/InMemoryMailEventRepository.ts +1 -1
  156. package/core/server/services/members-events/LastSeenAtUpdater.js +1 -1
  157. package/core/server/services/offers/service.js +1 -1
  158. package/core/server/services/recommendations/RecommendationServiceWrapper.js +8 -8
  159. package/core/server/services/recommendations/service/BookshelfClickEventRepository.js +48 -0
  160. package/core/server/services/recommendations/service/BookshelfClickEventRepository.ts +49 -0
  161. package/core/server/services/recommendations/service/BookshelfRecommendationRepository.js +98 -0
  162. package/core/server/services/recommendations/service/BookshelfRecommendationRepository.ts +117 -0
  163. package/core/server/services/recommendations/service/BookshelfRepository.js +134 -0
  164. package/core/server/services/recommendations/service/BookshelfRepository.ts +196 -0
  165. package/core/server/services/recommendations/service/BookshelfSubscribeEventRepository.js +48 -0
  166. package/core/server/services/recommendations/service/BookshelfSubscribeEventRepository.ts +49 -0
  167. package/core/server/services/recommendations/service/ClickEvent.js +33 -0
  168. package/core/server/services/recommendations/service/ClickEvent.ts +32 -0
  169. package/core/server/services/recommendations/service/InMemoryRecommendationRepository.js +19 -0
  170. package/core/server/services/recommendations/service/InMemoryRecommendationRepository.ts +20 -0
  171. package/core/server/services/recommendations/service/IncomingRecommendationController.js +34 -0
  172. package/core/server/services/recommendations/service/IncomingRecommendationController.ts +51 -0
  173. package/core/server/services/recommendations/service/IncomingRecommendationEmailRenderer.js +25 -0
  174. package/core/server/services/recommendations/service/IncomingRecommendationEmailRenderer.ts +37 -0
  175. package/core/server/services/recommendations/service/IncomingRecommendationService.js +93 -0
  176. package/core/server/services/recommendations/service/IncomingRecommendationService.ts +160 -0
  177. package/core/server/services/recommendations/service/Recommendation.js +140 -0
  178. package/core/server/services/recommendations/service/Recommendation.ts +201 -0
  179. package/core/server/services/recommendations/service/RecommendationController.js +208 -0
  180. package/core/server/services/recommendations/service/RecommendationController.ts +258 -0
  181. package/core/server/services/recommendations/service/RecommendationMetadataService.js +86 -0
  182. package/core/server/services/recommendations/service/RecommendationMetadataService.ts +128 -0
  183. package/core/server/services/recommendations/service/RecommendationRepository.js +2 -0
  184. package/core/server/services/recommendations/service/RecommendationRepository.ts +13 -0
  185. package/core/server/services/recommendations/service/RecommendationService.js +228 -0
  186. package/core/server/services/recommendations/service/RecommendationService.ts +281 -0
  187. package/core/server/services/recommendations/service/SubscribeEvent.js +33 -0
  188. package/core/server/services/recommendations/service/SubscribeEvent.ts +32 -0
  189. package/core/server/services/recommendations/service/UnsafeData.js +183 -0
  190. package/core/server/services/recommendations/service/UnsafeData.ts +217 -0
  191. package/core/server/services/recommendations/service/WellknownService.js +36 -0
  192. package/core/server/services/recommendations/service/WellknownService.ts +47 -0
  193. package/core/server/services/recommendations/service/index.js +31 -0
  194. package/core/server/services/recommendations/service/index.ts +15 -0
  195. package/core/server/services/recommendations/service/libraries.d.ts +5 -0
  196. package/core/server/services/slack-notifications/SlackNotifications.js +211 -0
  197. package/core/server/services/slack-notifications/SlackNotificationsService.js +90 -0
  198. package/core/server/services/slack-notifications/service.js +4 -6
  199. package/core/server/web/api/endpoints/admin/app.js +1 -21
  200. package/core/server/web/api/middleware/version-match.js +41 -0
  201. package/core/shared/labs.js +2 -2
  202. package/package.json +87 -104
  203. package/tsconfig.tsbuildinfo +1 -1
  204. package/yarn.lock +1470 -1540
  205. package/components/tryghost-adapter-cache-redis-5.115.1.tgz +0 -0
  206. package/components/tryghost-adapter-manager-5.115.1.tgz +0 -0
  207. package/components/tryghost-announcement-bar-settings-5.115.1.tgz +0 -0
  208. package/components/tryghost-constants-5.115.1.tgz +0 -0
  209. package/components/tryghost-custom-fonts-5.115.1.tgz +0 -0
  210. package/components/tryghost-data-generator-5.115.1.tgz +0 -0
  211. package/components/tryghost-email-addresses-5.115.1.tgz +0 -0
  212. package/components/tryghost-email-content-generator-5.115.1.tgz +0 -0
  213. package/components/tryghost-email-events-5.115.1.tgz +0 -0
  214. package/components/tryghost-email-service-5.115.1.tgz +0 -0
  215. package/components/tryghost-email-suppression-list-5.115.1.tgz +0 -0
  216. package/components/tryghost-express-dynamic-redirects-5.115.1.tgz +0 -0
  217. package/components/tryghost-ghost-5.115.1.tgz +0 -0
  218. package/components/tryghost-html-to-plaintext-5.115.1.tgz +0 -0
  219. package/components/tryghost-i18n-5.115.1.tgz +0 -0
  220. package/components/tryghost-importer-handler-content-files-5.115.1.tgz +0 -0
  221. package/components/tryghost-in-memory-repository-5.115.1.tgz +0 -0
  222. package/components/tryghost-job-manager-5.115.1.tgz +0 -0
  223. package/components/tryghost-link-redirects-5.115.1.tgz +0 -0
  224. package/components/tryghost-link-replacer-5.115.1.tgz +0 -0
  225. package/components/tryghost-magic-link-5.115.1.tgz +0 -0
  226. package/components/tryghost-mailgun-client-5.115.1.tgz +0 -0
  227. package/components/tryghost-member-events-5.115.1.tgz +0 -0
  228. package/components/tryghost-members-api-5.115.1.tgz +0 -0
  229. package/components/tryghost-members-csv-5.115.1.tgz +0 -0
  230. package/components/tryghost-members-payments-5.115.1.tgz +0 -0
  231. package/components/tryghost-minifier-5.115.1.tgz +0 -0
  232. package/components/tryghost-mw-version-match-5.115.1.tgz +0 -0
  233. package/components/tryghost-mw-vhost-5.115.1.tgz +0 -0
  234. package/components/tryghost-posts-service-5.115.1.tgz +0 -0
  235. package/components/tryghost-recommendations-5.115.1.tgz +0 -0
  236. package/components/tryghost-security-5.115.1.tgz +0 -0
  237. package/components/tryghost-slack-notifications-5.115.1.tgz +0 -0
  238. package/components/tryghost-webmentions-5.115.1.tgz +0 -0
  239. package/core/built/admin/assets/chunk.524.2439684964c164c598ab.js +0 -35
  240. package/core/built/admin/assets/chunk.582.bf5a2bbb2c4eb69ef1e7.js +0 -37
  241. package/core/built/admin/assets/ghost-327b17ea23cb8c89bd7e6a51e18e8506.css +0 -1
  242. package/core/built/admin/assets/ghost-dark-f30a597ac19632a118939492591c531b.css +0 -1
  243. /package/core/built/admin/assets/{chunk.874.461cb3cf5b6b36915f8c.js.LICENSE.txt → chunk.713.e9027c0cc3c56110f5da.js.LICENSE.txt} +0 -0
@@ -0,0 +1,217 @@
1
+ import errors from '@tryghost/errors';
2
+
3
+ type UnsafeDataContext = {
4
+ field?: string[]
5
+ }
6
+
7
+ function serializeField(field: string[]) {
8
+ if (field.length === 0) {
9
+ return 'data';
10
+ }
11
+ return field.join('.');
12
+ }
13
+
14
+ type NullData = {
15
+ readonly string: null,
16
+ readonly boolean: null,
17
+ readonly number: null,
18
+ readonly integer: null,
19
+ readonly url: null
20
+ enum(allowedValues: unknown[]): null
21
+ key(key: string): NullData
22
+ optionalKey(key: string): NullData
23
+ readonly array: null
24
+ index(index: number): NullData
25
+ }
26
+
27
+ /**
28
+ * NOTE: should be moved to a separate package in case this pattern is found to be useful
29
+ */
30
+ export class UnsafeData {
31
+ protected data: unknown;
32
+ protected context: UnsafeDataContext;
33
+
34
+ constructor(data: unknown, context: UnsafeDataContext = {}) {
35
+ this.data = data;
36
+ this.context = context;
37
+ }
38
+
39
+ protected get field() {
40
+ return serializeField(this.context.field ?? []);
41
+ }
42
+
43
+ protected addKeyToField(key: string) {
44
+ return this.context.field ? [...this.context.field, key] : [key];
45
+ }
46
+
47
+ protected fieldWithKey(key: string) {
48
+ return serializeField(this.addKeyToField(key));
49
+ }
50
+
51
+ /**
52
+ * Returns undefined if the key is not present on the object. Note that this doesn't check for null.
53
+ */
54
+ optionalKey(key: string): UnsafeData|undefined {
55
+ if (typeof this.data !== 'object' || this.data === null) {
56
+ throw new errors.ValidationError({message: `${this.field} must be an object`});
57
+ }
58
+
59
+ if (!Object.prototype.hasOwnProperty.call(this.data, key)) {
60
+ return undefined;
61
+ }
62
+
63
+ return new UnsafeData((this.data as Record<string, unknown>)[key], {
64
+ field: this.addKeyToField(key)
65
+ });
66
+ }
67
+
68
+ key(key: string): UnsafeData {
69
+ if (typeof this.data !== 'object' || this.data === null) {
70
+ throw new errors.ValidationError({message: `${this.field} must be an object`});
71
+ }
72
+
73
+ if (!Object.prototype.hasOwnProperty.call(this.data, key)) {
74
+ throw new errors.ValidationError({message: `${this.fieldWithKey(key)} is required`});
75
+ }
76
+
77
+ return new UnsafeData((this.data as Record<string, unknown>)[key], {
78
+ field: this.addKeyToField(key)
79
+ });
80
+ }
81
+
82
+ /**
83
+ * Use this to get a nullable value:
84
+ * ```
85
+ * const url: string|null = data.key('url').nullable.string
86
+ * ```
87
+ */
88
+ get nullable(): UnsafeData|NullData {
89
+ if (this.data === null) {
90
+ const d: NullData = {
91
+ get string() {
92
+ return null;
93
+ },
94
+ get boolean() {
95
+ return null;
96
+ },
97
+ get number() {
98
+ return null;
99
+ },
100
+ get integer() {
101
+ return null;
102
+ },
103
+ get url() {
104
+ return null;
105
+ },
106
+ enum() {
107
+ return null;
108
+ },
109
+ key() {
110
+ return d;
111
+ },
112
+ optionalKey() {
113
+ return d;
114
+ },
115
+ get array() {
116
+ return null;
117
+ },
118
+ index() {
119
+ return d;
120
+ }
121
+ };
122
+ return d;
123
+ }
124
+ return this;
125
+ }
126
+
127
+ get string(): string {
128
+ if (typeof this.data !== 'string') {
129
+ throw new errors.ValidationError({message: `${this.field} must be a string`});
130
+ }
131
+ return this.data;
132
+ }
133
+
134
+ get boolean(): boolean {
135
+ if (typeof this.data !== 'boolean') {
136
+ throw new errors.ValidationError({message: `${this.field} must be a boolean`});
137
+ }
138
+ return this.data;
139
+ }
140
+
141
+ get number(): number {
142
+ if (typeof this.data === 'string') {
143
+ const parsed = parseFloat(this.data);
144
+ if (isNaN(parsed) || parsed.toString() !== this.data) {
145
+ throw new errors.ValidationError({message: `${this.field} must be a number, got ${typeof this.data}`});
146
+ }
147
+ return new UnsafeData(parsed, this.context).number;
148
+ }
149
+
150
+ if (typeof this.data !== 'number') {
151
+ throw new errors.ValidationError({message: `${this.field} must be a number, got ${typeof this.data}`});
152
+ }
153
+ if (Number.isNaN(this.data) || !Number.isFinite(this.data)) {
154
+ throw new errors.ValidationError({message: `${this.field} must be a finite number`});
155
+ }
156
+ return this.data;
157
+ }
158
+
159
+ get integer(): number {
160
+ if (typeof this.data === 'string') {
161
+ const parsed = parseInt(this.data);
162
+ if (isNaN(parsed) || parsed.toString() !== this.data) {
163
+ throw new errors.ValidationError({message: `${this.field} must be an integer`});
164
+ }
165
+ return new UnsafeData(parseInt(this.data), this.context).integer;
166
+ }
167
+
168
+ const number = this.number;
169
+ if (!Number.isSafeInteger(number)) {
170
+ throw new errors.ValidationError({message: `${this.field} must be an integer`});
171
+ }
172
+ return number;
173
+ }
174
+
175
+ get url(): URL {
176
+ if (this.data instanceof URL) {
177
+ return this.data;
178
+ }
179
+
180
+ const string = this.string;
181
+ try {
182
+ const url = new URL(string);
183
+
184
+ if (!['http:', 'https:'].includes(url.protocol)) {
185
+ throw new errors.ValidationError({message: `${this.field} must be a valid URL`});
186
+ }
187
+ return url;
188
+ } catch (e) {
189
+ throw new errors.ValidationError({message: `${this.field} must be a valid URL`});
190
+ }
191
+ }
192
+
193
+ enum<T>(allowedValues: T[]): T {
194
+ if (!allowedValues.includes(this.data as T)) {
195
+ throw new errors.ValidationError({message: `${this.field} must be one of ${allowedValues.join(', ')}`});
196
+ }
197
+ return this.data as T;
198
+ }
199
+
200
+ get array(): UnsafeData[] {
201
+ if (!Array.isArray(this.data)) {
202
+ throw new errors.ValidationError({message: `${this.field} must be an array`});
203
+ }
204
+ return this.data.map((d, i) => new UnsafeData(d, {field: this.addKeyToField(`${i}`)}));
205
+ }
206
+
207
+ index(index: number) {
208
+ const arr = this.array;
209
+ if (index < 0 || !Number.isSafeInteger(index)) {
210
+ throw new errors.IncorrectUsageError({message: `index must be a positive integer`});
211
+ }
212
+ if (index >= arr.length) {
213
+ throw new errors.ValidationError({message: `${this.field} must be an array of length ${index + 1}`});
214
+ }
215
+ return arr[index];
216
+ }
217
+ }
@@ -0,0 +1,36 @@
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.WellknownService = void 0;
7
+ const promises_1 = __importDefault(require("fs/promises"));
8
+ const path_1 = __importDefault(require("path"));
9
+ class WellknownService {
10
+ dir;
11
+ urlUtils;
12
+ constructor({ dir, urlUtils }) {
13
+ this.dir = dir;
14
+ this.urlUtils = urlUtils;
15
+ }
16
+ #formatRecommendation(recommendation) {
17
+ return {
18
+ url: recommendation.url,
19
+ updated_at: (recommendation.updatedAt ?? recommendation.createdAt).toISOString(),
20
+ created_at: (recommendation.createdAt).toISOString()
21
+ };
22
+ }
23
+ getPath() {
24
+ return path_1.default.join(this.dir, '/.well-known/recommendations.json');
25
+ }
26
+ getURL() {
27
+ return new URL(this.urlUtils.relativeToAbsolute('/.well-known/recommendations.json'));
28
+ }
29
+ async set(recommendations) {
30
+ const content = JSON.stringify(recommendations.map(r => this.#formatRecommendation(r)));
31
+ const path = this.getPath();
32
+ await promises_1.default.mkdir(path_1.default.dirname(path), { recursive: true });
33
+ await promises_1.default.writeFile(path, content);
34
+ }
35
+ }
36
+ exports.WellknownService = WellknownService;
@@ -0,0 +1,47 @@
1
+ import {Recommendation} from './Recommendation';
2
+ import fs from 'fs/promises';
3
+ import _path from 'path';
4
+
5
+ type UrlUtils = {
6
+ relativeToAbsolute(url: string): string
7
+ }
8
+ type Options = {
9
+ /**
10
+ * Where to publish the wellknown file
11
+ */
12
+ dir: string,
13
+ urlUtils: UrlUtils
14
+ }
15
+
16
+ export class WellknownService {
17
+ dir: string;
18
+ urlUtils: UrlUtils;
19
+
20
+ constructor({dir, urlUtils}: Options) {
21
+ this.dir = dir;
22
+ this.urlUtils = urlUtils;
23
+ }
24
+
25
+ #formatRecommendation(recommendation: Recommendation) {
26
+ return {
27
+ url: recommendation.url,
28
+ updated_at: (recommendation.updatedAt ?? recommendation.createdAt).toISOString(),
29
+ created_at: (recommendation.createdAt).toISOString()
30
+ };
31
+ }
32
+
33
+ getPath() {
34
+ return _path.join(this.dir, '/.well-known/recommendations.json');
35
+ }
36
+
37
+ getURL(): URL {
38
+ return new URL(this.urlUtils.relativeToAbsolute('/.well-known/recommendations.json'));
39
+ }
40
+
41
+ async set(recommendations: Recommendation[]) {
42
+ const content = JSON.stringify(recommendations.map(r => this.#formatRecommendation(r)));
43
+ const path = this.getPath();
44
+ await fs.mkdir(_path.dirname(path), {recursive: true});
45
+ await fs.writeFile(path, content);
46
+ }
47
+ }
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./RecommendationController"), exports);
18
+ __exportStar(require("./RecommendationService"), exports);
19
+ __exportStar(require("./RecommendationRepository"), exports);
20
+ __exportStar(require("./InMemoryRecommendationRepository"), exports);
21
+ __exportStar(require("./Recommendation"), exports);
22
+ __exportStar(require("./WellknownService"), exports);
23
+ __exportStar(require("./BookshelfRecommendationRepository"), exports);
24
+ __exportStar(require("./ClickEvent"), exports);
25
+ __exportStar(require("./BookshelfClickEventRepository"), exports);
26
+ __exportStar(require("./SubscribeEvent"), exports);
27
+ __exportStar(require("./BookshelfSubscribeEventRepository"), exports);
28
+ __exportStar(require("./IncomingRecommendationController"), exports);
29
+ __exportStar(require("./IncomingRecommendationService"), exports);
30
+ __exportStar(require("./IncomingRecommendationEmailRenderer"), exports);
31
+ __exportStar(require("./RecommendationMetadataService"), exports);
@@ -0,0 +1,15 @@
1
+ export * from './RecommendationController';
2
+ export * from './RecommendationService';
3
+ export * from './RecommendationRepository';
4
+ export * from './InMemoryRecommendationRepository';
5
+ export * from './Recommendation';
6
+ export * from './WellknownService';
7
+ export * from './BookshelfRecommendationRepository';
8
+ export * from './ClickEvent';
9
+ export * from './BookshelfClickEventRepository';
10
+ export * from './SubscribeEvent';
11
+ export * from './BookshelfSubscribeEventRepository';
12
+ export * from './IncomingRecommendationController';
13
+ export * from './IncomingRecommendationService';
14
+ export * from './IncomingRecommendationEmailRenderer';
15
+ export * from './RecommendationMetadataService';
@@ -0,0 +1,5 @@
1
+ declare module '@tryghost/errors';
2
+ declare module '@tryghost/tpl';
3
+ declare module '@tryghost/logging';
4
+ declare module '@tryghost/mongo-utils';
5
+ declare module '@tryghost/nql';
@@ -0,0 +1,211 @@
1
+ const got = require('got');
2
+ const validator = require('@tryghost/validator');
3
+ const errors = require('@tryghost/errors');
4
+ const ghostVersion = require('@tryghost/version');
5
+ const moment = require('moment');
6
+
7
+ /**
8
+ * @typedef {URL} webhookUrl
9
+ */
10
+
11
+ /**
12
+ * @typedef {string} siteUrl
13
+ */
14
+
15
+ /**
16
+ * @typedef {import('@tryghost/logging')} logging
17
+ */
18
+
19
+ /**
20
+ * @typedef {import('./SlackNotificationsService').ISlackNotifications} ISlackNotifications
21
+ */
22
+
23
+ /**
24
+ * @implements {ISlackNotifications}
25
+ */
26
+ class SlackNotifications {
27
+ /** @type {URL} */
28
+ #webhookUrl;
29
+
30
+ /** @type {siteUrl} */
31
+ #siteUrl;
32
+
33
+ /** @type {logging} */
34
+ #logging;
35
+
36
+ /**
37
+ * @param {object} deps
38
+ * @param {URL} deps.webhookUrl
39
+ * @param {siteUrl} deps.siteUrl
40
+ * @param {logging} deps.logging
41
+ */
42
+ constructor(deps) {
43
+ this.#siteUrl = deps.siteUrl;
44
+ this.#webhookUrl = deps.webhookUrl;
45
+ this.#logging = deps.logging;
46
+ }
47
+
48
+ /**
49
+ * @param {object} eventData
50
+ * @param {import('@tryghost/milestones/lib/InMemoryMilestoneRepository').Milestone} eventData.milestone
51
+ * @param {object} [eventData.meta]
52
+ * @param {'import'|'email'|'skipped'|'initial'} [eventData.meta.reason]
53
+ * @param {number} [eventData.meta.currentValue]
54
+ *
55
+ * @returns {Promise<void>}
56
+ */
57
+ async notifyMilestoneReceived({milestone, meta}) {
58
+ if (meta?.reason === 'skipped' || meta?.reason === 'initial') {
59
+ return;
60
+ }
61
+ const hasImportedMembers = meta?.reason === 'import' ? 'has imported members' : null;
62
+ const lastEmailTooSoon = meta?.reason === 'email' ? 'last email too recent' : null;
63
+ const emailNotSentReason = hasImportedMembers || lastEmailTooSoon;
64
+ const milestoneTypePretty = milestone.type === 'arr' ? 'ARR' : 'Members';
65
+ const valueFormatted = this.#getFormattedAmount({amount: milestone.value, currency: milestone?.currency});
66
+ const emailSentText = milestone?.emailSentAt ? this.#getFormattedDate(milestone?.emailSentAt) : `no / ${emailNotSentReason}`;
67
+ const title = `:tada: ${milestoneTypePretty} Milestone ${valueFormatted} reached!`;
68
+
69
+ let valueSection;
70
+
71
+ if (milestone.type === 'arr') {
72
+ valueSection = {
73
+ type: 'section',
74
+ fields: [
75
+ {
76
+ type: 'mrkdwn',
77
+ text: `*Milestone:*\n${valueFormatted}`
78
+ }
79
+
80
+ ]
81
+ };
82
+
83
+ if (meta?.currentValue) {
84
+ valueSection.fields.push({
85
+ type: 'mrkdwn',
86
+ text: `*Current ARR:*\n${this.#getFormattedAmount({amount: meta.currentValue, currency: milestone?.currency})}`
87
+ });
88
+ }
89
+ } else {
90
+ valueSection = {
91
+ type: 'section',
92
+ fields: [
93
+ {
94
+ type: 'mrkdwn',
95
+ text: `*Milestone:*\n${valueFormatted}`
96
+ }
97
+ ]
98
+ };
99
+ if (meta?.currentValue) {
100
+ valueSection.fields.push({
101
+ type: 'mrkdwn',
102
+ text: `*Current Members:*\n${this.#getFormattedAmount({amount: meta.currentValue})}`
103
+ });
104
+ }
105
+ }
106
+
107
+ const blocks = [
108
+ {
109
+ type: 'header',
110
+ text: {
111
+ type: 'plain_text',
112
+ text: title,
113
+ emoji: true
114
+ }
115
+ },
116
+ {
117
+ type: 'section',
118
+ text: {
119
+ type: 'mrkdwn',
120
+ text: `New *${milestoneTypePretty} Milestone* achieved for <${this.#siteUrl}|${this.#siteUrl}>`
121
+ }
122
+ },
123
+ {
124
+ type: 'divider'
125
+ },
126
+ valueSection,
127
+ {
128
+ type: 'section',
129
+ text: {
130
+ type: 'mrkdwn',
131
+ text: `*Email sent:*\n${emailSentText}`
132
+ }
133
+ }
134
+ ];
135
+
136
+ const slackData = {
137
+ unfurl_links: false,
138
+ username: 'Ghost Milestone Service',
139
+ attachments: [
140
+ {
141
+ color: '#36a64f',
142
+ blocks
143
+ }
144
+ ]
145
+ };
146
+
147
+ await this.send(slackData, this.#webhookUrl);
148
+ }
149
+
150
+ /**
151
+ *
152
+ * @param {object} slackData
153
+ * @param {URL} url
154
+ *
155
+ * @returns {Promise<any>}
156
+ */
157
+ async send(slackData, url) {
158
+ if ((!url || typeof url !== 'string') || !validator.isURL(url)) {
159
+ const err = new errors.InternalServerError({
160
+ message: 'URL empty or invalid.',
161
+ code: 'URL_MISSING_INVALID',
162
+ context: url
163
+ });
164
+
165
+ return this.#logging.error(err);
166
+ }
167
+
168
+ const requestOptions = {
169
+ body: JSON.stringify(slackData),
170
+ headers: {
171
+ 'user-agent': 'Ghost/' + ghostVersion.original + ' (https://github.com/TryGhost/Ghost)'
172
+ }
173
+ };
174
+
175
+ if (process.env.NODE_ENV?.startsWith('test')) {
176
+ requestOptions.retry = 0;
177
+ }
178
+
179
+ return await got.post(url, requestOptions);
180
+ }
181
+
182
+ /**
183
+ * @param {object} options
184
+ * @param {number} options.amount
185
+ * @param {string} [options.currency]
186
+ *
187
+ * @returns {string}
188
+ */
189
+ #getFormattedAmount({amount = 0, currency}) {
190
+ if (!currency) {
191
+ return Intl.NumberFormat().format(amount);
192
+ }
193
+
194
+ return Intl.NumberFormat('en', {
195
+ style: 'currency',
196
+ currency,
197
+ currencyDisplay: 'symbol'
198
+ }).format(amount);
199
+ }
200
+
201
+ /**
202
+ * @param {string|Date} date
203
+ *
204
+ * @returns {string}
205
+ */
206
+ #getFormattedDate(date) {
207
+ return moment(date).format('D MMM YYYY');
208
+ }
209
+ }
210
+
211
+ module.exports = SlackNotifications;
@@ -0,0 +1,90 @@
1
+ const {MilestoneCreatedEvent} = require('@tryghost/milestones');
2
+
3
+ /**
4
+ * @typedef {import('@tryghost/milestones/lib/InMemoryMilestoneRepository').Milestone} Milestone
5
+ */
6
+
7
+ /**
8
+ * @typedef {object} meta
9
+ * @prop {'import'|'email'} [reason]
10
+ * @prop {number} [currentValue]
11
+ */
12
+
13
+ /**
14
+ * @typedef {import('@tryghost/logging')} logging
15
+ */
16
+
17
+ /**
18
+ * @typedef {object} ISlackNotifications
19
+ * @param {logging} logging
20
+ * @param {URL} siteUrl
21
+ * @param {URL} webhookUrl
22
+ * @prop {Object.<Milestone, ?meta>} notifyMilestoneReceived
23
+ * @prop {(slackData: object, url: URL) => Promise<void>} send
24
+ */
25
+
26
+ /**
27
+ * @typedef {object} config
28
+ * @prop {boolean} isEnabled
29
+ * @prop {URL} webhookUrl
30
+ * @prop {number} minThreshold
31
+ */
32
+
33
+ module.exports = class SlackNotificationsService {
34
+ /** @type {import('@tryghost/domain-events')} */
35
+ #DomainEvents;
36
+
37
+ /** @type {import('@tryghost/logging')} */
38
+ #logging;
39
+
40
+ /** @type {config} */
41
+ #config;
42
+
43
+ /** @type {ISlackNotifications} */
44
+ #slackNotifications;
45
+
46
+ /**
47
+ *
48
+ * @param {object} deps
49
+ * @param {import('@tryghost/domain-events')} deps.DomainEvents
50
+ * @param {config} deps.config
51
+ * @param {import('@tryghost/logging')} deps.logging
52
+ * @param {ISlackNotifications} deps.slackNotifications
53
+ */
54
+ constructor(deps) {
55
+ this.#DomainEvents = deps.DomainEvents;
56
+ this.#logging = deps.logging;
57
+ this.#config = deps.config;
58
+ this.#slackNotifications = deps.slackNotifications;
59
+ }
60
+
61
+ /**
62
+ *
63
+ * @param {MilestoneCreatedEvent} type
64
+ * @param {object} event
65
+ * @param {object} event.data
66
+ *
67
+ * @returns {Promise<void>}
68
+ */
69
+ async #handleEvent(type, event) {
70
+ if (
71
+ type === MilestoneCreatedEvent
72
+ && event.data.milestone
73
+ && this.#config.isEnabled
74
+ && this.#config.webhookUrl
75
+ && this.#config.minThreshold < event.data.milestone.value
76
+ ) {
77
+ try {
78
+ await this.#slackNotifications.notifyMilestoneReceived(event.data);
79
+ } catch (error) {
80
+ this.#logging.error(error);
81
+ }
82
+ }
83
+ }
84
+
85
+ subscribeEvents() {
86
+ this.#DomainEvents.subscribe(MilestoneCreatedEvent, async (event) => {
87
+ await this.#handleEvent(MilestoneCreatedEvent, event);
88
+ });
89
+ }
90
+ };
@@ -3,7 +3,7 @@ const config = require('../../../shared/config');
3
3
  const logging = require('@tryghost/logging');
4
4
 
5
5
  class SlackNotificationsServiceWrapper {
6
- /** @type {import('@tryghost/slack-notifications/lib/SlackNotificationsService')} */
6
+ /** @type {import('./SlackNotificationsService')} */
7
7
  #api;
8
8
 
9
9
  /**
@@ -14,13 +14,11 @@ class SlackNotificationsServiceWrapper {
14
14
  * @param {URL} deps.webhookUrl
15
15
  * @param {number} deps.minThreshold
16
16
  *
17
- * @returns {import('@tryghost/slack-notifications/lib/SlackNotificationsService')}
17
+ * @returns {import('./SlackNotificationsService')}
18
18
  */
19
19
  static create({siteUrl, isEnabled, webhookUrl, minThreshold}) {
20
- const {
21
- SlackNotificationsService,
22
- SlackNotifications
23
- } = require('@tryghost/slack-notifications');
20
+ const SlackNotificationsService = require('./SlackNotificationsService');
21
+ const SlackNotifications = require('./SlackNotifications');
24
22
 
25
23
  const slackNotifications = new SlackNotifications({
26
24
  webhookUrl,