ghost 5.119.2 → 5.120.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 (130) hide show
  1. package/components/tryghost-i18n-5.120.0.tgz +0 -0
  2. package/core/boot.js +0 -2
  3. package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +7555 -7216
  4. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-60ce658c.mjs → CodeEditorView-1c5b0683.mjs} +2 -2
  5. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +2 -2
  6. package/core/built/admin/assets/admin-x-settings/{index-8480baa8.mjs → index-14e518a7.mjs} +3 -3
  7. package/core/built/admin/assets/admin-x-settings/{index-a2648c61.mjs → index-fc9f985b.mjs} +2 -2
  8. package/core/built/admin/assets/admin-x-settings/{modals-6900c1d5.mjs → modals-15bc6a0f.mjs} +7192 -6656
  9. package/core/built/admin/assets/{chunk.137.c9bf40f01afeeadb4660.js → chunk.383.25fca2f09b4896656125.js} +76 -59
  10. package/core/built/admin/assets/chunk.524.1657b12c0ab25dd9fb79.js +28 -0
  11. package/core/built/admin/assets/{chunk.582.98a820cbc4bb65f2e685.js → chunk.582.09869b1f1a3cc0ab81f6.js} +19 -26
  12. package/core/built/admin/assets/{ghost-843572e9507d099162ae744d791daba1.js → ghost-b3b44421acca3b3eec76bfbb6ba0e81b.js} +3 -3
  13. package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +12578 -12352
  14. package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +423 -211
  15. package/core/built/admin/assets/posts/posts.js +13680 -13671
  16. package/core/built/admin/assets/stats/stats.js +16457 -16635
  17. package/core/built/admin/assets/{vendor-8f805740fee4db959a5b2119001a56b1.js → vendor-4ce6d282a2a00fe486a0951e0591da19.js} +11 -9
  18. package/core/built/admin/index.html +5 -5
  19. package/core/frontend/helpers/match.js +6 -0
  20. package/core/frontend/services/routing/ParentRouter.js +1 -1
  21. package/core/frontend/services/routing/controllers/email-post.js +0 -2
  22. package/core/frontend/services/routing/controllers/previews.js +0 -3
  23. package/core/frontend/web/middleware/frontend-caching.js +2 -2
  24. package/core/server/api/endpoints/authentication.js +37 -73
  25. package/core/server/api/endpoints/authors-public.js +8 -9
  26. package/core/server/api/endpoints/db.js +34 -35
  27. package/core/server/api/endpoints/emails.js +8 -10
  28. package/core/server/api/endpoints/integrations.js +20 -18
  29. package/core/server/api/endpoints/invites.js +8 -10
  30. package/core/server/api/endpoints/labels.js +19 -23
  31. package/core/server/api/endpoints/notifications.js +3 -4
  32. package/core/server/api/endpoints/pages-public.js +8 -10
  33. package/core/server/api/endpoints/pages.js +14 -18
  34. package/core/server/api/endpoints/posts-public.js +8 -10
  35. package/core/server/api/endpoints/posts.js +6 -8
  36. package/core/server/api/endpoints/previews.js +8 -10
  37. package/core/server/api/endpoints/redirects.js +7 -8
  38. package/core/server/api/endpoints/schedules.js +5 -7
  39. package/core/server/api/endpoints/slugs.js +7 -9
  40. package/core/server/api/endpoints/snippets.js +16 -20
  41. package/core/server/api/endpoints/tags-public.js +8 -10
  42. package/core/server/api/endpoints/tags.js +19 -23
  43. package/core/server/api/endpoints/themes.js +6 -8
  44. package/core/server/api/endpoints/users.js +31 -36
  45. package/core/server/api/endpoints/utils/permissions.js +10 -10
  46. package/core/server/api/endpoints/utils/serializers/output/roles.js +9 -10
  47. package/core/server/api/endpoints/utils/validators/input/images.js +43 -52
  48. package/core/server/api/endpoints/utils/validators/input/invites.js +6 -8
  49. package/core/server/api/endpoints/webhooks.js +38 -42
  50. package/core/server/data/migrations/versions/5.120/2025-05-07-14-57-38-add-newsletters-button-corners-column.js +8 -0
  51. package/core/server/data/migrations/versions/5.120/2025-05-13-17-36-56-add-newsletters-button-style-column.js +8 -0
  52. package/core/server/data/migrations/versions/5.120/2025-05-14-20-00-15-add-newsletters-setting-columns.js +22 -0
  53. package/core/server/data/schema/schema.js +6 -1
  54. package/core/server/lib/image/Gravatar.js +12 -13
  55. package/core/server/lib/lexical.js +3 -1
  56. package/core/server/models/newsletter.js +6 -1
  57. package/core/server/services/api-version-compatibility/index.js +1 -33
  58. package/core/server/services/auth/session/emails/signin.js +3 -3
  59. package/core/server/services/email-address/EmailAddressParser.js +52 -0
  60. package/core/server/services/email-address/EmailAddressParser.js.d.ts +13 -0
  61. package/core/server/services/email-address/EmailAddressService.js +142 -0
  62. package/core/server/services/email-address/EmailAddressService.ts +183 -0
  63. package/core/server/services/email-address/EmailAddressServiceWrapper.js +2 -4
  64. package/core/server/services/email-analytics/EmailAnalyticsService.js +1 -1
  65. package/core/server/services/email-analytics/EmailAnalyticsServiceWrapper.js +2 -1
  66. package/core/server/services/email-service/BatchSendingService.js +703 -0
  67. package/core/server/services/email-service/EmailBodyCache.js +20 -0
  68. package/core/server/services/email-service/EmailController.js +94 -0
  69. package/core/server/services/email-service/EmailEventProcessor.js +267 -0
  70. package/core/server/services/email-service/EmailEventStorage.js +187 -0
  71. package/core/server/services/email-service/EmailRenderer.js +1263 -0
  72. package/core/server/services/email-service/EmailSegmenter.js +74 -0
  73. package/core/server/services/email-service/EmailService.js +310 -0
  74. package/core/server/services/email-service/EmailServiceWrapper.js +9 -2
  75. package/core/server/services/email-service/MailgunEmailProvider.js +191 -0
  76. package/core/server/services/email-service/SendingService.js +173 -0
  77. package/core/server/services/email-service/email-templates/partials/feedback-button.hbs +7 -0
  78. package/core/server/services/email-service/email-templates/partials/latest-posts.hbs +39 -0
  79. package/core/server/services/email-service/email-templates/partials/paywall.hbs +20 -0
  80. package/core/server/services/email-service/email-templates/partials/styles.hbs +2348 -0
  81. package/core/server/services/email-service/email-templates/template.hbs +238 -0
  82. package/core/server/services/email-service/events/EmailBouncedEvent.js +63 -0
  83. package/core/server/services/email-service/events/EmailDeliveredEvent.js +49 -0
  84. package/core/server/services/email-service/events/EmailOpenedEvent.js +49 -0
  85. package/core/server/services/email-service/events/EmailTemporaryBouncedEvent.js +63 -0
  86. package/core/server/services/email-service/events/EmailUnsubscribedEvent.js +42 -0
  87. package/core/server/services/email-service/events/SpamComplaintEvent.js +42 -0
  88. package/core/server/services/email-service/helpers/register-helpers.js +59 -0
  89. package/core/server/services/email-suppression-list/MailgunEmailSuppressionList.js +2 -1
  90. package/core/server/services/explore-ping/index.js +2 -1
  91. package/core/server/services/mail/GhostMailer.js +1 -1
  92. package/core/server/services/media-inliner/ExternalMediaInliner.js +2 -1
  93. package/core/server/services/members/api.js +15 -15
  94. package/core/server/services/members/emails/signin.js +4 -4
  95. package/core/server/services/members/emails/signup-paid.js +3 -4
  96. package/core/server/services/members/emails/signup.js +3 -3
  97. package/core/server/services/members/emails/subscribe.js +3 -3
  98. package/core/server/services/members/members-api/controllers/RouterController.js +50 -36
  99. package/core/server/services/members/members-api/repositories/MemberRepository.js +92 -92
  100. package/core/server/services/members-events/LastSeenAtUpdater.js +1 -1
  101. package/core/server/services/settings-helpers/SettingsHelpers.js +1 -1
  102. package/core/server/services/staff/StaffServiceEmails.js +1 -1
  103. package/core/server/services/stats/PostsStatsService.js +28 -7
  104. package/core/server/web/api/app.js +0 -1
  105. package/core/server/web/api/endpoints/admin/app.js +0 -2
  106. package/core/server/web/api/endpoints/content/app.js +0 -2
  107. package/core/server/web/api/middleware/upload.js +2 -2
  108. package/core/shared/custom-theme-settings-cache/CustomThemeSettingsService.js +2 -1
  109. package/package.json +39 -97
  110. package/tsconfig.tsbuildinfo +1 -1
  111. package/yarn.lock +385 -517
  112. package/components/tryghost-api-framework-5.119.2.tgz +0 -0
  113. package/components/tryghost-custom-fonts-5.119.2.tgz +0 -0
  114. package/components/tryghost-domain-events-5.119.2.tgz +0 -0
  115. package/components/tryghost-email-addresses-5.119.2.tgz +0 -0
  116. package/components/tryghost-email-service-5.119.2.tgz +0 -0
  117. package/components/tryghost-html-to-plaintext-5.119.2.tgz +0 -0
  118. package/components/tryghost-i18n-5.119.2.tgz +0 -0
  119. package/components/tryghost-job-manager-5.119.2.tgz +0 -0
  120. package/components/tryghost-members-csv-5.119.2.tgz +0 -0
  121. package/components/tryghost-mw-error-handler-5.119.2.tgz +0 -0
  122. package/components/tryghost-mw-vhost-5.119.2.tgz +0 -0
  123. package/components/tryghost-prometheus-metrics-5.119.2.tgz +0 -0
  124. package/components/tryghost-security-5.119.2.tgz +0 -0
  125. package/core/built/admin/assets/chunk.524.b8545af3bb714bc4f820.js +0 -35
  126. package/core/server/services/api-version-compatibility/APIVersionCompatibilityService.js +0 -99
  127. package/core/server/services/api-version-compatibility/VersionNotificationsDataService.js +0 -80
  128. package/core/server/services/api-version-compatibility/extract-api-key.js +0 -57
  129. package/core/server/services/api-version-compatibility/mw-api-version-mismatch.js +0 -31
  130. /package/core/built/admin/assets/{chunk.137.c9bf40f01afeeadb4660.js.LICENSE.txt → chunk.383.25fca2f09b4896656125.js.LICENSE.txt} +0 -0
@@ -913,35 +913,35 @@ module.exports = class MemberRepository {
913
913
  options.batch_id = ObjectId().toHexString();
914
914
  }
915
915
 
916
- const member = await this._Member.findOne({
916
+ const memberModel = await this._Member.findOne({
917
917
  id: data.id
918
918
  }, {...options, forUpdate: true});
919
919
 
920
- const customer = await member.related('stripeCustomers').query({
920
+ const memberStripeCustomerModel = await memberModel.related('stripeCustomers').query({
921
921
  where: {
922
922
  customer_id: data.subscription.customer
923
923
  }
924
924
  }).fetchOne(options);
925
925
 
926
- if (!customer) {
926
+ if (!memberStripeCustomerModel) {
927
927
  // Maybe just link the customer?
928
928
  throw new errors.NotFoundError({message: tpl(messages.subscriptionNotFound)});
929
929
  }
930
930
 
931
- const subscription = await this._stripeAPIService.getSubscription(data.subscription.id);
931
+ const stripeSubscriptionData = await this._stripeAPIService.getSubscription(data.subscription.id);
932
932
  let paymentMethodId;
933
- if (!subscription.default_payment_method) {
933
+ if (!stripeSubscriptionData.default_payment_method) {
934
934
  paymentMethodId = null;
935
- } else if (typeof subscription.default_payment_method === 'string') {
936
- paymentMethodId = subscription.default_payment_method;
935
+ } else if (typeof stripeSubscriptionData.default_payment_method === 'string') {
936
+ paymentMethodId = stripeSubscriptionData.default_payment_method;
937
937
  } else {
938
- paymentMethodId = subscription.default_payment_method.id;
938
+ paymentMethodId = stripeSubscriptionData.default_payment_method.id;
939
939
  }
940
- const paymentMethod = paymentMethodId ? await this._stripeAPIService.getCardPaymentMethod(paymentMethodId) : null;
940
+ const stripePaymentMethodData = paymentMethodId ? await this._stripeAPIService.getCardPaymentMethod(paymentMethodId) : null;
941
941
 
942
- const model = await this.getSubscriptionByStripeID(subscription.id, {...options, forUpdate: true});
942
+ const stripeCustomerSubscriptionModel = await this.getSubscriptionByStripeID(stripeSubscriptionData.id, {...options, forUpdate: true});
943
943
 
944
- const subscriptionPriceData = _.get(subscription, 'items.data[0].price');
944
+ const subscriptionPriceData = _.get(stripeSubscriptionData, 'items.data[0].price');
945
945
  let ghostProduct;
946
946
  try {
947
947
  ghostProduct = await this._productRepository.get({stripe_product_id: subscriptionPriceData.product}, options);
@@ -973,14 +973,14 @@ module.exports = class MemberRepository {
973
973
  }, options);
974
974
  } else {
975
975
  // Log error if no Ghost products found
976
- logging.error(`There was an error linking subscription - ${subscription.id}, no Products exist.`);
976
+ logging.error(`There was an error linking subscription - ${stripeSubscriptionData.id}, no Products exist.`);
977
977
  }
978
978
  } catch (e) {
979
- logging.error(`Failed to handle prices and product for - ${subscription.id}.`);
979
+ logging.error(`Failed to handle prices and product for - ${stripeSubscriptionData.id}.`);
980
980
  logging.error(e);
981
981
  }
982
982
 
983
- let stripeCouponId = subscription.discount && subscription.discount.coupon ? subscription.discount.coupon.id : null;
983
+ let stripeCouponId = stripeSubscriptionData.discount && stripeSubscriptionData.discount.coupon ? stripeSubscriptionData.discount.coupon.id : null;
984
984
 
985
985
  // For trial offers, offer id is passed from metadata as there is no stripe coupon
986
986
  let offerId = data.offerId || null;
@@ -992,26 +992,26 @@ module.exports = class MemberRepository {
992
992
  if (offer) {
993
993
  offerId = offer.id;
994
994
  } else {
995
- logging.error(`Received an unknown stripe coupon id (${stripeCouponId}) for subscription - ${subscription.id}.`);
995
+ logging.error(`Received an unknown stripe coupon id (${stripeCouponId}) for subscription - ${stripeSubscriptionData.id}.`);
996
996
  }
997
997
  } else if (offerId) {
998
998
  offer = await this._offerRepository.getById(offerId, {transacting: options.transacting});
999
999
  }
1000
1000
 
1001
1001
  const subscriptionData = {
1002
- customer_id: subscription.customer,
1003
- subscription_id: subscription.id,
1004
- status: subscription.status,
1005
- cancel_at_period_end: subscription.cancel_at_period_end,
1006
- cancellation_reason: this.getCancellationReason(subscription),
1007
- current_period_end: new Date(subscription.current_period_end * 1000),
1008
- start_date: new Date(subscription.start_date * 1000),
1009
- default_payment_card_last4: paymentMethod && paymentMethod.card && paymentMethod.card.last4 || null,
1002
+ customer_id: stripeSubscriptionData.customer,
1003
+ subscription_id: stripeSubscriptionData.id,
1004
+ status: stripeSubscriptionData.status,
1005
+ cancel_at_period_end: stripeSubscriptionData.cancel_at_period_end,
1006
+ cancellation_reason: this.getCancellationReason(stripeSubscriptionData),
1007
+ current_period_end: new Date(stripeSubscriptionData.current_period_end * 1000),
1008
+ start_date: new Date(stripeSubscriptionData.start_date * 1000),
1009
+ default_payment_card_last4: stripePaymentMethodData && stripePaymentMethodData.card && stripePaymentMethodData.card.last4 || null,
1010
1010
  stripe_price_id: subscriptionPriceData.id,
1011
1011
  plan_id: subscriptionPriceData.id,
1012
1012
  // trial start and end are returned as Stripe timestamps and need coversion
1013
- trial_start_at: subscription.trial_start ? new Date(subscription.trial_start * 1000) : null,
1014
- trial_end_at: subscription.trial_end ? new Date(subscription.trial_end * 1000) : null,
1013
+ trial_start_at: stripeSubscriptionData.trial_start ? new Date(stripeSubscriptionData.trial_start * 1000) : null,
1014
+ trial_end_at: stripeSubscriptionData.trial_end ? new Date(stripeSubscriptionData.trial_end * 1000) : null,
1015
1015
  // NOTE: Defaulting to interval as migration to nullable field
1016
1016
  // turned out to be much bigger problem.
1017
1017
  // Ideally, would need nickname field to be nullable on the DB level
@@ -1023,9 +1023,9 @@ module.exports = class MemberRepository {
1023
1023
  mrr: this.getMRR({
1024
1024
  interval: _.get(subscriptionPriceData, 'recurring.interval', ''),
1025
1025
  amount: subscriptionPriceData.unit_amount,
1026
- status: subscription.status,
1027
- canceled: subscription.cancel_at_period_end,
1028
- discount: subscription.discount
1026
+ status: stripeSubscriptionData.status,
1027
+ canceled: stripeSubscriptionData.cancel_at_period_end,
1028
+ discount: stripeSubscriptionData.discount
1029
1029
  }),
1030
1030
  offer_id: offerId
1031
1031
  };
@@ -1050,44 +1050,44 @@ module.exports = class MemberRepository {
1050
1050
  };
1051
1051
  let eventData = {};
1052
1052
 
1053
- const shouldBeDeleted = subscription.metadata && !!subscription.metadata.ghost_migrated_to && subscription.status === 'canceled';
1054
- if (shouldBeDeleted) {
1053
+ const stripeCustomerSubscriptionModelShouldBeDeleted = stripeSubscriptionData.metadata && !!stripeSubscriptionData.metadata.ghost_migrated_to && stripeSubscriptionData.status === 'canceled';
1054
+ if (stripeCustomerSubscriptionModelShouldBeDeleted) {
1055
1055
  logging.warn(`Subscription ${subscriptionData.subscription_id} is marked for deletion, skipping linking.`);
1056
1056
 
1057
- if (model) {
1057
+ if (stripeCustomerSubscriptionModel) {
1058
1058
  // Delete all paid subscription events manually for this subscription
1059
1059
  // This is the only related event without a foreign key constraint
1060
- await this._MemberPaidSubscriptionEvent.query().where('subscription_id', model.id).delete().transacting(options.transacting);
1060
+ await this._MemberPaidSubscriptionEvent.query().where('subscription_id', stripeCustomerSubscriptionModel.id).delete().transacting(options.transacting);
1061
1061
 
1062
1062
  // Delete the subscription in the database because we don't want to show it in the UI or in our data calculations
1063
- await model.destroy(options);
1063
+ await stripeCustomerSubscriptionModel.destroy(options);
1064
1064
  }
1065
- } else if (model) {
1065
+ } else if (stripeCustomerSubscriptionModel) {
1066
1066
  // CASE: Offer is already mapped against sub, don't overwrite it with NULL
1067
1067
  // Needed for trial offers, which don't have a stripe coupon/discount attached to sub
1068
1068
  if (!subscriptionData.offer_id) {
1069
1069
  delete subscriptionData.offer_id;
1070
1070
  }
1071
- const updated = await this._StripeCustomerSubscription.edit(subscriptionData, {
1071
+ const updatedStripeCustomerSubscriptionModel = await this._StripeCustomerSubscription.edit(subscriptionData, {
1072
1072
  ...options,
1073
- id: model.id
1073
+ id: stripeCustomerSubscriptionModel.id
1074
1074
  });
1075
1075
 
1076
1076
  // CASE: Existing free member subscribes to a paid tier with an offer
1077
1077
  // Stripe doesn't send the discount/offer info in the subscription.created event
1078
1078
  // So we need to record the offer redemption event upon updating the subscription here
1079
- if (model.get('offer_id') === null && subscriptionData.offer_id) {
1080
- const event = OfferRedemptionEvent.create({
1081
- memberId: member.id,
1079
+ if (stripeCustomerSubscriptionModel.get('offer_id') === null && subscriptionData.offer_id) {
1080
+ const offerRedemptionEvent = OfferRedemptionEvent.create({
1081
+ memberId: memberModel.id,
1082
1082
  offerId: subscriptionData.offer_id,
1083
- subscriptionId: updated.id
1084
- }, updated.get('created_at'));
1085
- this.dispatchEvent(event, options);
1083
+ subscriptionId: updatedStripeCustomerSubscriptionModel.id
1084
+ }, updatedStripeCustomerSubscriptionModel.get('created_at'));
1085
+ this.dispatchEvent(offerRedemptionEvent, options);
1086
1086
  }
1087
1087
 
1088
- if (model.get('mrr') !== updated.get('mrr') || model.get('plan_id') !== updated.get('plan_id') || model.get('status') !== updated.get('status') || model.get('cancel_at_period_end') !== updated.get('cancel_at_period_end')) {
1089
- const originalMrrDelta = model.get('mrr');
1090
- const updatedMrrDelta = updated.get('mrr');
1088
+ if (stripeCustomerSubscriptionModel.get('mrr') !== updatedStripeCustomerSubscriptionModel.get('mrr') || stripeCustomerSubscriptionModel.get('plan_id') !== updatedStripeCustomerSubscriptionModel.get('plan_id') || stripeCustomerSubscriptionModel.get('status') !== updatedStripeCustomerSubscriptionModel.get('status') || stripeCustomerSubscriptionModel.get('cancel_at_period_end') !== updatedStripeCustomerSubscriptionModel.get('cancel_at_period_end')) {
1089
+ const originalMrrDelta = stripeCustomerSubscriptionModel.get('mrr');
1090
+ const updatedMrrDelta = updatedStripeCustomerSubscriptionModel.get('mrr');
1091
1091
 
1092
1092
  const getEventType = (originalStatus, updatedStatus) => {
1093
1093
  if (originalStatus === updatedStatus) {
@@ -1101,19 +1101,19 @@ module.exports = class MemberRepository {
1101
1101
  return updatedStatus;
1102
1102
  };
1103
1103
 
1104
- const originalStatus = getStatus(model);
1105
- const updatedStatus = getStatus(updated);
1104
+ const originalStatus = getStatus(stripeCustomerSubscriptionModel);
1105
+ const updatedStatus = getStatus(updatedStripeCustomerSubscriptionModel);
1106
1106
  const eventType = getEventType(originalStatus, updatedStatus);
1107
1107
 
1108
1108
  const mrrDelta = updatedMrrDelta - originalMrrDelta;
1109
1109
 
1110
1110
  await this._MemberPaidSubscriptionEvent.add({
1111
- member_id: member.id,
1111
+ member_id: memberModel.id,
1112
1112
  source: 'stripe',
1113
1113
  type: eventType,
1114
- subscription_id: updated.id,
1115
- from_plan: model.get('plan_id'),
1116
- to_plan: updated.get('status') === 'canceled' ? null : updated.get('plan_id'),
1114
+ subscription_id: updatedStripeCustomerSubscriptionModel.id,
1115
+ from_plan: stripeCustomerSubscriptionModel.get('plan_id'),
1116
+ to_plan: updatedStripeCustomerSubscriptionModel.get('status') === 'canceled' ? null : updatedStripeCustomerSubscriptionModel.get('plan_id'),
1117
1117
  currency: subscriptionPriceData.currency,
1118
1118
  mrr_delta: mrrDelta
1119
1119
  }, options);
@@ -1125,15 +1125,15 @@ module.exports = class MemberRepository {
1125
1125
  const context = options?.context || {};
1126
1126
  const source = this._resolveContextSource(context);
1127
1127
 
1128
- const event = SubscriptionActivatedEvent.create({
1128
+ const subscriptionActivatedEvent = SubscriptionActivatedEvent.create({
1129
1129
  source,
1130
1130
  tierId: ghostProduct?.get('id'),
1131
- memberId: member.id,
1132
- subscriptionId: updated.get('id'),
1131
+ memberId: memberModel.id,
1132
+ subscriptionId: updatedStripeCustomerSubscriptionModel.get('id'),
1133
1133
  offerId: offerId,
1134
1134
  batchId: options.batch_id
1135
1135
  });
1136
- this.dispatchEvent(event, options);
1136
+ this.dispatchEvent(subscriptionActivatedEvent, options);
1137
1137
  }
1138
1138
 
1139
1139
  // Dispatch cancellation event, i.e. send paid cancellation staff notification, if:
@@ -1143,53 +1143,53 @@ module.exports = class MemberRepository {
1143
1143
  const context = options?.context || {};
1144
1144
  const source = this._resolveContextSource(context);
1145
1145
  const cancelNow = updatedStatus === 'expired';
1146
- const canceledAt = new Date(subscription.canceled_at * 1000);
1147
- const expiryAt = cancelNow ? canceledAt : updated.get('current_period_end');
1146
+ const canceledAt = new Date(stripeSubscriptionData.canceled_at * 1000);
1147
+ const expiryAt = cancelNow ? canceledAt : updatedStripeCustomerSubscriptionModel.get('current_period_end');
1148
1148
 
1149
- const event = SubscriptionCancelledEvent.create({
1149
+ const subscriptionCancelledEvent = SubscriptionCancelledEvent.create({
1150
1150
  source,
1151
1151
  tierId: ghostProduct?.get('id'),
1152
- memberId: member.id,
1153
- subscriptionId: updated.get('id'),
1152
+ memberId: memberModel.id,
1153
+ subscriptionId: updatedStripeCustomerSubscriptionModel.get('id'),
1154
1154
  cancelNow,
1155
1155
  canceledAt,
1156
1156
  expiryAt
1157
1157
  });
1158
1158
 
1159
- this.dispatchEvent(event, options);
1159
+ this.dispatchEvent(subscriptionCancelledEvent, options);
1160
1160
  }
1161
1161
  }
1162
1162
  } else {
1163
- eventData.created_at = new Date(subscription.start_date * 1000);
1164
- const subscriptionModel = await this._StripeCustomerSubscription.add(subscriptionData, options);
1163
+ eventData.created_at = new Date(stripeSubscriptionData.start_date * 1000);
1164
+ const newStripeCustomerSubscriptionModel = await this._StripeCustomerSubscription.add(subscriptionData, options);
1165
1165
  await this._MemberPaidSubscriptionEvent.add({
1166
- member_id: member.id,
1167
- subscription_id: subscriptionModel.id,
1166
+ member_id: memberModel.id,
1167
+ subscription_id: newStripeCustomerSubscriptionModel.id,
1168
1168
  type: 'created',
1169
1169
  source: 'stripe',
1170
1170
  from_plan: null,
1171
1171
  to_plan: subscriptionPriceData.id,
1172
1172
  currency: subscriptionPriceData.currency,
1173
- mrr_delta: subscriptionModel.get('mrr'),
1173
+ mrr_delta: newStripeCustomerSubscriptionModel.get('mrr'),
1174
1174
  ...eventData
1175
1175
  }, options);
1176
1176
 
1177
1177
  const context = options?.context || {};
1178
1178
  const source = this._resolveContextSource(context);
1179
1179
  const attribution = {
1180
- id: data.attribution?.id ?? subscription.metadata?.attribution_id ?? null,
1181
- url: data.attribution?.url ?? subscription.metadata?.attribution_url ?? null,
1182
- type: data.attribution?.type ?? subscription.metadata?.attribution_type ?? null,
1183
- referrerSource: data.attribution?.referrerSource ?? subscription.metadata?.referrer_source ?? null,
1184
- referrerMedium: data.attribution?.referrerMedium ?? subscription.metadata?.referrer_medium ?? null,
1185
- referrerUrl: data.attribution?.referrerUrl ?? subscription.metadata?.referrer_url ?? null
1180
+ id: data.attribution?.id ?? stripeSubscriptionData.metadata?.attribution_id ?? null,
1181
+ url: data.attribution?.url ?? stripeSubscriptionData.metadata?.attribution_url ?? null,
1182
+ type: data.attribution?.type ?? stripeSubscriptionData.metadata?.attribution_type ?? null,
1183
+ referrerSource: data.attribution?.referrerSource ?? stripeSubscriptionData.metadata?.referrer_source ?? null,
1184
+ referrerMedium: data.attribution?.referrerMedium ?? stripeSubscriptionData.metadata?.referrer_medium ?? null,
1185
+ referrerUrl: data.attribution?.referrerUrl ?? stripeSubscriptionData.metadata?.referrer_url ?? null
1186
1186
  };
1187
1187
 
1188
1188
  const subscriptionCreatedEvent = SubscriptionCreatedEvent.create({
1189
1189
  source,
1190
1190
  tierId: ghostProduct?.get('id'),
1191
- memberId: member.id,
1192
- subscriptionId: subscriptionModel.get('id'),
1191
+ memberId: memberModel.id,
1192
+ subscriptionId: newStripeCustomerSubscriptionModel.get('id'),
1193
1193
  offerId: offerId,
1194
1194
  attribution: attribution,
1195
1195
  batchId: options.batch_id
@@ -1199,53 +1199,53 @@ module.exports = class MemberRepository {
1199
1199
 
1200
1200
  if (offerId) {
1201
1201
  const offerRedemptionEvent = OfferRedemptionEvent.create({
1202
- memberId: member.id,
1202
+ memberId: memberModel.id,
1203
1203
  offerId: offerId,
1204
- subscriptionId: subscriptionModel.get('id')
1204
+ subscriptionId: newStripeCustomerSubscriptionModel.get('id')
1205
1205
  });
1206
1206
  this.dispatchEvent(offerRedemptionEvent, options);
1207
1207
  }
1208
1208
 
1209
- if (getStatus(subscriptionModel) === 'active') {
1210
- const activatedEvent = SubscriptionActivatedEvent.create({
1209
+ if (getStatus(newStripeCustomerSubscriptionModel) === 'active') {
1210
+ const subscriptionActivatedEvent = SubscriptionActivatedEvent.create({
1211
1211
  source,
1212
1212
  tierId: ghostProduct?.get('id'),
1213
- memberId: member.id,
1214
- subscriptionId: subscriptionModel.get('id'),
1213
+ memberId: memberModel.id,
1214
+ subscriptionId: newStripeCustomerSubscriptionModel.get('id'),
1215
1215
  offerId: offerId,
1216
1216
  attribution: attribution,
1217
1217
  batchId: options.batch_id
1218
1218
  });
1219
- this.dispatchEvent(activatedEvent, options);
1219
+ this.dispatchEvent(subscriptionActivatedEvent, options);
1220
1220
  }
1221
1221
  }
1222
1222
 
1223
- let memberProducts = (await member.related('products').fetch(options)).toJSON();
1224
- const oldMemberProducts = member.related('products').toJSON();
1223
+ let memberProducts = (await memberModel.related('products').fetch(options)).toJSON();
1224
+ const oldMemberProducts = memberModel.related('products').toJSON();
1225
1225
  let status = memberProducts.length === 0 ? 'free' : 'comped';
1226
- if (!shouldBeDeleted && this.isActiveSubscriptionStatus(subscription.status)) {
1227
- if (this.isComplimentarySubscription(subscription)) {
1226
+ if (!stripeCustomerSubscriptionModelShouldBeDeleted && this.isActiveSubscriptionStatus(stripeSubscriptionData.status)) {
1227
+ if (this.isComplimentarySubscription(stripeSubscriptionData)) {
1228
1228
  status = 'comped';
1229
1229
  } else {
1230
1230
  status = 'paid';
1231
1231
  }
1232
1232
 
1233
- if (model) {
1233
+ if (stripeCustomerSubscriptionModel) {
1234
1234
  // We might need to...
1235
1235
  // 1. delete the previous product from the linked member products (in case an existing subscription changed product/price)
1236
1236
  // 2. fix the list of products linked to a member (an existing subscription doesn't have a linked product to this member)
1237
1237
 
1238
- const subscriptions = await member.related('stripeSubscriptions').fetch(options);
1238
+ const subscriptions = await memberModel.related('stripeSubscriptions').fetch(options);
1239
1239
 
1240
1240
  const previousProduct = await this._productRepository.get({
1241
- stripe_price_id: model.get('stripe_price_id')
1241
+ stripe_price_id: stripeCustomerSubscriptionModel.get('stripe_price_id')
1242
1242
  }, options);
1243
1243
 
1244
1244
  if (previousProduct) {
1245
1245
  let activeSubscriptionForPreviousProduct = false;
1246
1246
 
1247
1247
  for (const subscriptionModel of subscriptions.models) {
1248
- if (this.isActiveSubscriptionStatus(subscriptionModel.get('status')) && subscriptionModel.id !== model.id) {
1248
+ if (this.isActiveSubscriptionStatus(subscriptionModel.get('status')) && subscriptionModel.id !== stripeCustomerSubscriptionModel.id) {
1249
1249
  try {
1250
1250
  const subscriptionProduct = await this._productRepository.get({stripe_price_id: subscriptionModel.get('stripe_price_id')}, options);
1251
1251
  if (subscriptionProduct && previousProduct && subscriptionProduct.id === previousProduct.id) {
@@ -1280,7 +1280,7 @@ module.exports = class MemberRepository {
1280
1280
  memberProducts.push(ghostProduct.toJSON());
1281
1281
  }
1282
1282
  } else {
1283
- const subscriptions = await member.related('stripeSubscriptions').fetch(options);
1283
+ const subscriptions = await memberModel.related('stripeSubscriptions').fetch(options);
1284
1284
  let activeSubscriptionForGhostProduct = false;
1285
1285
  for (const subscriptionModel of subscriptions.models) {
1286
1286
  if (this.isActiveSubscriptionStatus(subscriptionModel.get('status'))) {
@@ -1338,7 +1338,7 @@ module.exports = class MemberRepository {
1338
1338
 
1339
1339
  for (const productToAdd of productsToAdd) {
1340
1340
  await this._MemberProductEvent.add({
1341
- member_id: member.id,
1341
+ member_id: memberModel.id,
1342
1342
  product_id: productToAdd,
1343
1343
  action: 'added'
1344
1344
  }, options);
@@ -1346,7 +1346,7 @@ module.exports = class MemberRepository {
1346
1346
 
1347
1347
  for (const productToRemove of productsToRemove) {
1348
1348
  await this._MemberProductEvent.add({
1349
- member_id: member.id,
1349
+ member_id: memberModel.id,
1350
1350
  product_id: productToRemove,
1351
1351
  action: 'removed'
1352
1352
  }, options);
@@ -1,7 +1,7 @@
1
1
  const {MemberPageViewEvent, MemberCommentEvent, MemberLinkClickEvent} = require('../../../shared/events');
2
2
  const moment = require('moment-timezone');
3
3
  const {IncorrectUsageError} = require('@tryghost/errors');
4
- const {EmailOpenedEvent} = require('@tryghost/email-service');
4
+ const EmailOpenedEvent = require('../../services/email-service/events/EmailOpenedEvent');
5
5
  const logging = require('@tryghost/logging');
6
6
  const LastSeenAtCache = require('./LastSeenAtCache');
7
7
 
@@ -1,6 +1,6 @@
1
1
  const tpl = require('@tryghost/tpl');
2
2
  const errors = require('@tryghost/errors');
3
- const {EmailAddressParser} = require('@tryghost/email-addresses');
3
+ const EmailAddressParser = require('../email-address/EmailAddressParser');
4
4
  const logging = require('@tryghost/logging');
5
5
  const crypto = require('crypto');
6
6
 
@@ -2,7 +2,7 @@ const {promises: fs, readFileSync} = require('fs');
2
2
  const path = require('path');
3
3
  const moment = require('moment');
4
4
  const glob = require('glob');
5
- const {EmailAddressParser} = require('@tryghost/email-addresses');
5
+ const EmailAddressParser = require('../email-address/EmailAddressParser');
6
6
 
7
7
  class StaffServiceEmails {
8
8
  constructor({logging, models, mailer, settingsHelpers, settingsCache, blogIcon, urlUtils, labs}) {
@@ -139,24 +139,45 @@ class PostsStatsService {
139
139
  const paidReferrersCTE = this._buildPaidReferrersSubquery(postId, options);
140
140
  const mrrReferrersCTE = this._buildMrrReferrersSubquery(postId, options);
141
141
 
142
- const baseReferrersQuery = this.knex('members_created_events as mce')
142
+ // First, let's get all sources from both tables using separate queries
143
+ // Then combine and group them in a cross-database compatible way
144
+ const membersCreatedSources = this.knex('members_created_events as mce')
143
145
  .select('mce.referrer_source as source')
146
+ .select('mce.referrer_url')
144
147
  .where('mce.attribution_id', postId)
145
148
  .where('mce.attribution_type', 'post')
146
- .union((qb) => {
147
- qb.select('msce.referrer_source as source')
148
- .from('members_subscription_created_events as msce')
149
- .where('msce.attribution_id', postId)
150
- .where('msce.attribution_type', 'post');
149
+ .whereNotNull('mce.referrer_source');
150
+
151
+ const membersSubscriptionSources = this.knex('members_subscription_created_events as msce')
152
+ .select('msce.referrer_source as source')
153
+ .select('msce.referrer_url')
154
+ .where('msce.attribution_id', postId)
155
+ .where('msce.attribution_type', 'post')
156
+ .whereNotNull('msce.referrer_source');
157
+
158
+ // Using a simpler combined query that works in SQLite
159
+ const allSources = this.knex.select('source', 'referrer_url')
160
+ .from(membersCreatedSources.as('sources1'))
161
+ .union(function () {
162
+ this.select('source', 'referrer_url')
163
+ .from(membersSubscriptionSources.as('sources2'));
151
164
  });
152
165
 
166
+ // Create the final CTE that we'll use to get all referrers
167
+ const allReferrersCTE = this.knex.select('source')
168
+ .select(this.knex.raw('MIN(referrer_url) as referrer_url'))
169
+ .from(allSources.as('all_sources'))
170
+ .groupBy('source');
171
+
172
+ // Now join all the data
153
173
  let query = this.knex
154
174
  .with('free_referrers', freeReferrersCTE)
155
175
  .with('paid_referrers', paidReferrersCTE)
156
176
  .with('mrr_referrers', mrrReferrersCTE)
157
- .with('all_referrers', baseReferrersQuery)
177
+ .with('all_referrers', allReferrersCTE)
158
178
  .select(
159
179
  'ar.source',
180
+ 'ar.referrer_url',
160
181
  this.knex.raw('COALESCE(fr.free_members, 0) as free_members'),
161
182
  this.knex.raw('COALESCE(pr.paid_members, 0) as paid_members'),
162
183
  this.knex.raw('COALESCE(mr.mrr, 0) as mrr')
@@ -24,7 +24,6 @@ module.exports = function setupApiApp() {
24
24
 
25
25
  // Error handling for requests to non-existent API versions
26
26
  apiApp.use(errorHandler.resourceNotFound);
27
- apiApp.use(APIVersionCompatibilityService.errorHandler);
28
27
  apiApp.use(errorHandler.handleJSONResponse(sentry));
29
28
 
30
29
  debug('Parent API setup end');
@@ -8,7 +8,6 @@ const shared = require('../../../shared');
8
8
  const express = require('../../../../../shared/express');
9
9
  const sentry = require('../../../../../shared/sentry');
10
10
  const routes = require('./routes');
11
- const APIVersionCompatibilityService = require('../../../../services/api-version-compatibility');
12
11
 
13
12
  /**
14
13
  * @returns {import('express').Application}
@@ -38,7 +37,6 @@ module.exports = function setupApiApp() {
38
37
 
39
38
  // API error handling
40
39
  apiApp.use(errorHandler.resourceNotFound);
41
- apiApp.use(APIVersionCompatibilityService.errorHandler);
42
40
  apiApp.use(errorHandler.handleJSONResponse(sentry));
43
41
 
44
42
  debug('Admin API setup end');
@@ -7,7 +7,6 @@ const config = require('../../../../../shared/config');
7
7
  const shared = require('../../../shared');
8
8
  const routes = require('./routes');
9
9
  const errorHandler = require('@tryghost/mw-error-handler');
10
- const apiVersionCompatibility = require('../../../../services/api-version-compatibility');
11
10
 
12
11
  /**
13
12
  * @returns {import('express').Application}
@@ -34,7 +33,6 @@ module.exports = function setupApiApp() {
34
33
 
35
34
  // API error handling
36
35
  apiApp.use(errorHandler.resourceNotFound);
37
- apiApp.use(apiVersionCompatibility.errorHandler);
38
36
  apiApp.use(errorHandler.handleJSONResponse(sentry));
39
37
 
40
38
  debug('Content API setup end');
@@ -164,7 +164,7 @@ const checkFileIsValid = (fileData, types, extensions) => {
164
164
  /**
165
165
  *
166
166
  * @param {String} filepath
167
- * @returns {String | null}
167
+ * @returns {Promise<String | null>}
168
168
  *
169
169
  * Reads the SVG file, sanitizes it, and writes the sanitized content back to the file.
170
170
  * Returns the sanitized content or null if the SVG could not be sanitized.
@@ -216,7 +216,7 @@ const sanitizeSvgContent = (content) => {
216
216
  *
217
217
  * @param {String} filepath
218
218
  * @param {Boolean} isZipped
219
- * @returns {String | null}
219
+ * @returns {Promise<String | null>}
220
220
  *
221
221
  * Reads .svg or .svgz files and returns the content as a string.
222
222
  *
@@ -167,8 +167,9 @@ module.exports = class CustomThemeSettingsService {
167
167
  // Private -----------------------------------------------------------------
168
168
 
169
169
  /**
170
+ * @param {string} name - name of the theme
170
171
  * @param {Object} theme - checked theme output from gscan
171
- * @returns {Array} - list of stored theme record objects
172
+ * @returns {Promise<Array>} - list of stored theme record objects
172
173
  * @private
173
174
  */
174
175
  async _syncRepositoryWithTheme(name, theme) {