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,255 @@
1
+ const {DonationPaymentEvent} = require('@tryghost/donations');
2
+ const _ = require('lodash');
3
+ const errors = require('@tryghost/errors');
4
+ const logging = require('@tryghost/logging');
5
+
6
+ /**
7
+ * Handles `checkout.session.completed` webhook events
8
+ *
9
+ * The `checkout.session.completed` event is triggered when a customer completes a checkout session.
10
+ *
11
+ * It is triggered for the following scenarios:
12
+ * - Subscription
13
+ * - Donation
14
+ * - Setup intent
15
+ *
16
+ * This service delegates the event to the appropriate handler based on the session mode and metadata.
17
+ *
18
+ * The `session` payload can be found here: https://docs.stripe.com/api/checkout/sessions/object
19
+ */
20
+ module.exports = class CheckoutSessionEventService {
21
+ /**
22
+ * @param {object} deps
23
+ * @param {import('../../StripeAPI')} deps.api
24
+ * @param {object} deps.memberRepository
25
+ * @param {object} deps.donationRepository
26
+ * @param {object} deps.staffServiceEmails
27
+ * @param {function} deps.sendSignupEmail
28
+ */
29
+ constructor(deps) {
30
+ this.api = deps.api;
31
+ this.deps = deps;
32
+ }
33
+
34
+ /**
35
+ * Handles a `checkout.session.completed` event
36
+ * Delegates to the appropriate handler based on the session mode and metadata
37
+ * @param {import('stripe').Stripe.Checkout.Session} session
38
+ */
39
+ async handleEvent(session) {
40
+ if (session.mode === 'setup') {
41
+ await this.handleSetupEvent(session);
42
+ }
43
+
44
+ if (session.mode === 'subscription') {
45
+ await this.handleSubscriptionEvent(session);
46
+ }
47
+
48
+ if (session.mode === 'payment' && session.metadata?.ghost_donation) {
49
+ await this.handleDonationEvent(session);
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Handles a `checkout.session.completed` event for a donation
55
+ * @param {import('stripe').Stripe.Checkout.Session} session
56
+ */
57
+ async handleDonationEvent(session) {
58
+ const donationField = session.custom_fields?.find(obj => obj?.key === 'donation_message');
59
+ const donationMessage = donationField?.text?.value ? donationField.text.value : null;
60
+ const amount = session.amount_total;
61
+ const currency = session.currency;
62
+
63
+ const memberRepository = this.deps.memberRepository;
64
+ const member = session.customer ? (await memberRepository.get({customer_id: session.customer})) : null;
65
+
66
+ const data = DonationPaymentEvent.create({
67
+ name: member?.get('name') ?? session.customer_details.name,
68
+ email: member?.get('email') ?? session.customer_details.email,
69
+ memberId: member?.id ?? null,
70
+ amount,
71
+ currency,
72
+ donationMessage,
73
+ attributionId: session.metadata?.attribution_id ?? null,
74
+ attributionUrl: session.metadata?.attribution_url ?? null,
75
+ attributionType: session.metadata?.attribution_type ?? null,
76
+ referrerSource: session.metadata?.referrer_source ?? null,
77
+ referrerMedium: session.metadata?.referrer_medium ?? null,
78
+ referrerUrl: session.metadata?.referrer_url ?? null
79
+ });
80
+
81
+ const donationRepository = this.deps.donationRepository;
82
+ await donationRepository.save(data);
83
+
84
+ const staffServiceEmails = this.deps.staffServiceEmails;
85
+ await staffServiceEmails.notifyDonationReceived({donationPaymentEvent: data});
86
+ }
87
+
88
+ /**
89
+ * Handles a `checkout.session.completed` event for a setup intent
90
+ *
91
+ * This is used when a customer adds or changes their payment method outside
92
+ * of the normal subscription flow.
93
+ * @param {import('stripe').Stripe.Checkout.Session} session
94
+ */
95
+ async handleSetupEvent(session) {
96
+ const setupIntent = await this.api.getSetupIntent(session.setup_intent);
97
+
98
+ const memberRepository = this.deps.memberRepository;
99
+ const member = await memberRepository.get({
100
+ customer_id: setupIntent.metadata.customer_id
101
+ });
102
+
103
+ if (!member) {
104
+ return;
105
+ }
106
+
107
+ await this.api.attachPaymentMethodToCustomer(
108
+ setupIntent.metadata.customer_id,
109
+ setupIntent.payment_method
110
+ );
111
+
112
+ if (setupIntent.metadata.subscription_id) {
113
+ const updatedSubscription = await this.api.updateSubscriptionDefaultPaymentMethod(
114
+ setupIntent.metadata.subscription_id,
115
+ setupIntent.payment_method
116
+ );
117
+ try {
118
+ await memberRepository.linkSubscription({
119
+ id: member.id,
120
+ subscription: updatedSubscription
121
+ });
122
+ } catch (err) {
123
+ if (err.code !== 'ER_DUP_ENTRY' && err.code !== 'SQLITE_CONSTRAINT') {
124
+ throw err;
125
+ }
126
+ throw new errors.ConflictError({
127
+ err
128
+ });
129
+ }
130
+ return;
131
+ }
132
+
133
+ const subscriptions = await member.related('stripeSubscriptions').fetch();
134
+ const activeSubscriptions = subscriptions.models.filter(subscription => ['active', 'trialing', 'unpaid', 'past_due'].includes(subscription.get('status'))
135
+ );
136
+
137
+ for (const subscription of activeSubscriptions) {
138
+ if (subscription.get('customer_id') === setupIntent.metadata.customer_id) {
139
+ const updatedSubscription = await this.api.updateSubscriptionDefaultPaymentMethod(
140
+ subscription.get('subscription_id'),
141
+ setupIntent.payment_method
142
+ );
143
+ try {
144
+ await memberRepository.linkSubscription({
145
+ id: member.id,
146
+ subscription: updatedSubscription
147
+ });
148
+ } catch (err) {
149
+ if (err.code !== 'ER_DUP_ENTRY' && err.code !== 'SQLITE_CONSTRAINT') {
150
+ throw err;
151
+ }
152
+ throw new errors.ConflictError({
153
+ err
154
+ });
155
+ }
156
+ }
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Handles a `checkout.session.completed` event for a subscription
162
+ * @param {import('stripe').Stripe.Checkout.Session} session
163
+ */
164
+ async handleSubscriptionEvent(session) {
165
+ const customer = await this.api.getCustomer(session.customer, {
166
+ expand: ['subscriptions.data.default_payment_method']
167
+ });
168
+
169
+ const memberRepository = this.deps.memberRepository;
170
+
171
+ let member = await memberRepository.get({
172
+ email: customer.email
173
+ });
174
+
175
+ const checkoutType = _.get(session, 'metadata.checkoutType');
176
+
177
+ if (!member) {
178
+ const metadataName = _.get(session, 'metadata.name');
179
+ const metadataNewsletters = _.get(session, 'metadata.newsletters');
180
+ const attribution = {
181
+ id: session.metadata?.attribution_id ?? null,
182
+ url: session.metadata?.attribution_url ?? null,
183
+ type: session.metadata?.attribution_type ?? null,
184
+ referrerSource: session.metadata?.referrer_source ?? null,
185
+ referrerMedium: session.metadata?.referrer_medium ?? null,
186
+ referrerUrl: session.metadata?.referrer_url ?? null
187
+ };
188
+
189
+ const payerName = _.get(customer, 'subscriptions.data[0].default_payment_method.billing_details.name');
190
+ const name = metadataName || payerName || null;
191
+
192
+ const memberData = {email: customer.email, name, attribution};
193
+ if (metadataNewsletters) {
194
+ try {
195
+ memberData.newsletters = JSON.parse(metadataNewsletters);
196
+ } catch (e) {
197
+ logging.error(`Ignoring invalid newsletters data - ${metadataNewsletters}.`);
198
+ }
199
+ }
200
+
201
+ const offerId = session.metadata?.offer;
202
+ const memberDataWithStripeCustomer = {
203
+ ...memberData,
204
+ stripeCustomer: customer,
205
+ offerId
206
+ };
207
+ member = await memberRepository.create(memberDataWithStripeCustomer);
208
+ } else {
209
+ const payerName = _.get(customer, 'subscriptions.data[0].default_payment_method.billing_details.name');
210
+ const attribution = {
211
+ id: session.metadata?.attribution_id ?? null,
212
+ url: session.metadata?.attribution_url ?? null,
213
+ type: session.metadata?.attribution_type ?? null,
214
+ referrerSource: session.metadata?.referrer_source ?? null,
215
+ referrerMedium: session.metadata?.referrer_medium ?? null,
216
+ referrerUrl: session.metadata?.referrer_url ?? null
217
+ };
218
+
219
+ if (payerName && !member.get('name')) {
220
+ await memberRepository.update({name: payerName}, {id: member.get('id')});
221
+ }
222
+
223
+ await memberRepository.upsertCustomer({
224
+ customer_id: customer.id,
225
+ member_id: member.id,
226
+ name: customer.name,
227
+ email: customer.email
228
+ });
229
+
230
+ for (const subscription of customer.subscriptions.data) {
231
+ try {
232
+ const offerId = session.metadata?.offer;
233
+
234
+ await memberRepository.linkSubscription({
235
+ id: member.id,
236
+ subscription,
237
+ offerId,
238
+ attribution
239
+ });
240
+ } catch (err) {
241
+ if (err.code !== 'ER_DUP_ENTRY' && err.code !== 'SQLITE_CONSTRAINT') {
242
+ throw err;
243
+ }
244
+ throw new errors.ConflictError({
245
+ err
246
+ });
247
+ }
248
+ }
249
+ }
250
+
251
+ if (checkoutType !== 'upgrade') {
252
+ this.deps.sendSignupEmail(customer.email);
253
+ }
254
+ }
255
+ };
@@ -0,0 +1,70 @@
1
+ const errors = require('@tryghost/errors');
2
+ // const _ = require('lodash');
3
+
4
+ /**
5
+ * Handles `invoice.payment_succeeded` webhook events
6
+ *
7
+ * The `invoice.payment_succeeded` event is triggered when a customer's payment succeeds.
8
+ */
9
+ module.exports = class InvoiceEventService {
10
+ /**
11
+ * @param {object} deps
12
+ * @param {object} deps.api
13
+ * @param {object} deps.memberRepository
14
+ * @param {object} deps.eventRepository
15
+ * @param {object} deps.productRepository
16
+ */
17
+ constructor(deps) {
18
+ this.deps = deps;
19
+ }
20
+
21
+ /**
22
+ * Handles a `invoice.payment_succeeded` event
23
+ *
24
+ * Inserts a payment event into the database
25
+ * @param {import('stripe').Stripe.Invoice} invoice
26
+ */
27
+ async handleInvoiceEvent(invoice) {
28
+ const {api, memberRepository, eventRepository, productRepository} = this.deps;
29
+
30
+ if (!invoice.subscription) {
31
+ // Check if this is a one time payment, related to a donation
32
+ // this is being handled in checkoutSessionEvent because we need to handle the custom donation message
33
+ // which is not available in the invoice object
34
+ return;
35
+ }
36
+ const subscription = await api.getSubscription(invoice.subscription, {
37
+ expand: ['default_payment_method']
38
+ });
39
+
40
+ const member = await memberRepository.get({
41
+ customer_id: subscription.customer
42
+ });
43
+
44
+ if (member) {
45
+ if (invoice.paid && invoice.amount_paid !== 0) {
46
+ await eventRepository.registerPayment({
47
+ member_id: member.id,
48
+ currency: invoice.currency,
49
+ amount: invoice.amount_paid
50
+ });
51
+ }
52
+ } else {
53
+ // Subscription has more than one plan - meaning it is not one created by us - ignore.
54
+ if (!subscription.plan) {
55
+ return;
56
+ }
57
+ // Subscription is for a different product - ignore.
58
+ const product = await productRepository.get({
59
+ stripe_product_id: subscription.plan.product
60
+ });
61
+ if (!product) {
62
+ return;
63
+ }
64
+ // Could not find the member, which we need in order to insert an payment event.
65
+ throw new errors.NotFoundError({
66
+ message: `No member found for customer ${subscription.customer}`
67
+ });
68
+ }
69
+ }
70
+ };
@@ -0,0 +1,54 @@
1
+ const errors = require('@tryghost/errors');
2
+ const _ = require('lodash');
3
+
4
+ /**
5
+ * Handles `customer.subscription.*` webhook events
6
+ *
7
+ * The `customer.subscription.*` events are triggered when a customer's subscription status changes.
8
+ *
9
+ * This service is responsible for handling these events and updating the subscription status in Ghost,
10
+ * although it mostly delegates the responsibility to the `MemberRepository`.
11
+ */
12
+ module.exports = class SubscriptionEventService {
13
+ /**
14
+ * @param {object} deps
15
+ * @param {import('../../repositories/MemberRepository')} deps.memberRepository
16
+ */
17
+ constructor(deps) {
18
+ this.deps = deps;
19
+ }
20
+
21
+ /**
22
+ * Handles a `customer.subscription.*` event
23
+ *
24
+ * Looks up the member by the Stripe customer ID and links the subscription to the member.
25
+ * @param {import('stripe').Stripe.Subscription} subscription
26
+ */
27
+ async handleSubscriptionEvent(subscription) {
28
+ const subscriptionPriceData = _.get(subscription, 'items.data');
29
+ if (!subscriptionPriceData || subscriptionPriceData.length !== 1) {
30
+ throw new errors.BadRequestError({
31
+ message: 'Subscription should have exactly 1 price item'
32
+ });
33
+ }
34
+
35
+ const memberRepository = this.deps.memberRepository;
36
+ const member = await memberRepository.get({
37
+ customer_id: subscription.customer
38
+ });
39
+
40
+ if (member) {
41
+ try {
42
+ await memberRepository.linkSubscription({
43
+ id: member.id,
44
+ subscription
45
+ });
46
+ } catch (err) {
47
+ if (err.code !== 'ER_DUP_ENTRY' && err.code !== 'SQLITE_CONSTRAINT') {
48
+ throw err;
49
+ }
50
+ throw new errors.ConflictError({err});
51
+ }
52
+ }
53
+ }
54
+ };
@@ -1,6 +1,6 @@
1
1
  const debug = require('@tryghost/debug')('themes');
2
- const packageJSON = require('@tryghost/package-json');
3
2
 
3
+ const packageJSON = require('../../lib/package-json');
4
4
  const config = require('../../../shared/config');
5
5
  const themeList = require('./list');
6
6
 
@@ -1,7 +1,7 @@
1
1
  const _ = require('lodash');
2
2
  const themeList = require('./list');
3
3
  const bridge = require('../../../bridge');
4
- const packageJSON = require('@tryghost/package-json');
4
+ const packageJSON = require('../../lib/package-json');
5
5
  const settingsCache = require('../../../shared/settings-cache');
6
6
 
7
7
  /**
@@ -168,6 +168,7 @@ module.exports = function apiRoutes() {
168
168
 
169
169
  // ## Slugs
170
170
  router.get('/slugs/:type/:name', mw.authAdminApi, http(api.slugs.generate));
171
+ router.get('/slugs/:type/:name/:id', mw.authAdminApi, http(api.slugs.generate));
171
172
 
172
173
  // ## Themes
173
174
  router.get('/themes/', mw.authAdminApi, http(api.themes.browse));
@@ -0,0 +1,51 @@
1
+ // # CacheControl Middleware
2
+ // Usage: cacheControl(profile), where profile is one of 'public' or 'private'
3
+ // After: checkIsPrivate
4
+ // Before: routes
5
+ // App: Admin|Site|API
6
+ //
7
+ // Allows each app to declare its own default caching rules
8
+
9
+ const isString = require('lodash/isString');
10
+
11
+ /**
12
+ * @param {'public'|'private'} profile Use "private" if you do not want caching
13
+ * @param {object} [options]
14
+ * @param {number} [options.maxAge] The max-age in seconds to use when profile is "public"
15
+ * @param {number} [options.staleWhileRevalidate] The stale-while-revalidate in seconds to use when profile is "public"
16
+ */
17
+ const cacheControl = (profile, options = {maxAge: 0}) => {
18
+ const isOptionHasProperty = property => Object.prototype.hasOwnProperty.call(options, property);
19
+ const publicOptions = [
20
+ 'public',
21
+ `max-age=${options.maxAge}`,
22
+ isOptionHasProperty('staleWhileRevalidate') ? `stale-while-revalidate=${options.staleWhileRevalidate}` : ''
23
+ ];
24
+
25
+ const profiles = {
26
+ public: publicOptions.filter(option => option).join(', '),
27
+ private: 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0'
28
+ };
29
+
30
+ let output;
31
+
32
+ if (isString(profile) && Object.prototype.hasOwnProperty.call(profiles, profile)) {
33
+ output = profiles[profile];
34
+ }
35
+
36
+ /**
37
+ * @param {import('express').Request} req
38
+ * @param {import('express').Response} res
39
+ * @param {() => void} next
40
+ *
41
+ * @returns {void}
42
+ */
43
+ return function cacheControlHeaders(req, res, next) {
44
+ if (output) {
45
+ res.set({'Cache-Control': output});
46
+ }
47
+ next();
48
+ };
49
+ };
50
+
51
+ module.exports = cacheControl;
@@ -8,7 +8,7 @@ module.exports = {
8
8
  },
9
9
 
10
10
  get cacheControl() {
11
- return require('@tryghost/mw-cache-control');
11
+ return require('./cache-control');
12
12
  },
13
13
 
14
14
  get prettyUrls() {
@@ -1,7 +1,7 @@
1
- const cacheControl = require('@tryghost/mw-cache-control');
2
1
  const express = require('../../shared/express');
3
2
  const settings = require('../../shared/settings-cache');
4
3
  const config = require('../../shared/config');
4
+ const {cacheControl} = require('./shared/middleware');
5
5
 
6
6
  module.exports = function setupWellKnownApp() {
7
7
  const wellKnownApp = express('well-known');
@@ -51,8 +51,10 @@ const ALPHA_FEATURES = [
51
51
  'lexicalIndicators',
52
52
  'adminXDemo',
53
53
  'postsX',
54
+ 'statsX',
54
55
  'captcha',
55
- 'contentVisibilityAlpha'
56
+ 'contentVisibilityAlpha',
57
+ 'explore'
56
58
  ];
57
59
 
58
60
  module.exports.GA_KEYS = [...GA_FEATURES];
@@ -11,6 +11,59 @@ const _ = require('lodash');
11
11
  * - See the notes in core/server/lib/common/events
12
12
  * - There's also a plan to introduce a proper caching layer, and rewrite this on top of that
13
13
  */
14
+
15
+ /**
16
+ * @typedef {Object} PublicSettingsCache
17
+ * @property {string|null} title - The blog's title
18
+ * @property {string|null} description - The blog's description
19
+ * @property {string|null} logo - URL to the blog's logo
20
+ * @property {string|null} icon - URL to the blog's icon
21
+ * @property {string|null} accent_color - The blog's accent color
22
+ * @property {string|null} cover_image - URL to the blog's cover image
23
+ * @property {string|null} facebook - Facebook page name
24
+ * @property {string|null} twitter - Twitter username
25
+ * @property {string|null} lang - The blog's language code
26
+ * @property {string|null} locale - The blog's locale
27
+ * @property {string|null} timezone - The blog's timezone
28
+ * @property {string|null} codeinjection_head - Code injected into head
29
+ * @property {string|null} codeinjection_foot - Code injected into footer
30
+ * @property {string|null} navigation - JSON string of navigation items
31
+ * @property {string|null} secondary_navigation - JSON string of secondary navigation items
32
+ * @property {string|null} meta_title - Custom meta title
33
+ * @property {string|null} meta_description - Custom meta description
34
+ * @property {string|null} og_image - Open Graph image URL
35
+ * @property {string|null} og_title - Open Graph title
36
+ * @property {string|null} og_description - Open Graph description
37
+ * @property {string|null} twitter_image - Twitter card image URL
38
+ * @property {string|null} twitter_title - Twitter card title
39
+ * @property {string|null} twitter_description - Twitter card description
40
+ * @property {string|null} members_support_address - Support email for members
41
+ * @property {boolean|null} members_enabled - Whether members feature is enabled
42
+ * @property {boolean|null} allow_self_signup - Whether self signup is allowed
43
+ * @property {boolean|null} members_invite_only - Whether membership is invite only
44
+ * @property {string|null} members_signup_access - Member signup access level
45
+ * @property {boolean|null} paid_members_enabled - Whether paid memberships are enabled
46
+ * @property {string|null} firstpromoter_account - FirstPromoter account ID
47
+ * @property {string|null} portal_button_style - Portal button style
48
+ * @property {string|null} portal_button_signup_text - Portal signup button text
49
+ * @property {string|null} portal_button_icon - Portal button icon
50
+ * @property {string|null} portal_signup_terms_html - Portal signup terms HTML
51
+ * @property {boolean|null} portal_signup_checkbox_required - Whether signup checkbox is required
52
+ * @property {string|null} portal_plans - JSON string of available portal plans
53
+ * @property {string|null} portal_default_plan - Default portal plan
54
+ * @property {boolean|null} portal_name - Whether to show portal names
55
+ * @property {boolean|null} portal_button - Whether to show the portal button
56
+ * @property {boolean|null} comments_enabled - Whether comments are enabled
57
+ * @property {boolean|null} recommendations_enabled - Whether recommendations are enabled
58
+ * @property {boolean|null} outbound_link_tagging - Whether outbound link tagging is enabled
59
+ * @property {string|null} default_email_address - Default email address
60
+ * @property {string|null} support_email_address - Support email address
61
+ * @property {string|null} editor_default_email_recipients - Default email recipients for editor
62
+ * @property {boolean|null} captcha_enabled - Whether captcha is enabled
63
+ * @property {string|null} labs - JSON string of enabled labs features
64
+ * @property {never} [x] - Prevent accessing undefined properties
65
+ */
66
+
14
67
  class CacheManager {
15
68
  /**
16
69
  * @prop {Object} options
@@ -164,14 +217,19 @@ class CacheManager {
164
217
  /**
165
218
  * Get all the publicly accessible cache entries with their correct names
166
219
  * Uses clone to prevent modifications from being reflected
167
- * @return {object} cache
220
+ * @return {PublicSettingsCache} cache
168
221
  */
169
222
  getPublic() {
170
- let settings = {};
171
-
172
- _.each(this.publicSettings, (key, newKey) => {
173
- settings[newKey] = this._doGet(key) ?? null;
174
- });
223
+ // This block correctly builds the type signature for the return value
224
+ /** @type {PublicSettingsCache} */
225
+ let settings = Object.fromEntries(
226
+ Object.keys(this.publicSettings).map(key => [this.publicSettings[key], null])
227
+ );
228
+
229
+ // This block correctly populates the values from the cache
230
+ for (const newKey in this.publicSettings) {
231
+ settings[newKey] = this._doGet(this.publicSettings[newKey]) ?? null;
232
+ }
175
233
 
176
234
  return settings;
177
235
  }