ghost 6.17.2 → 6.18.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 (211) hide show
  1. package/components/{tryghost-i18n-6.17.2.tgz → tryghost-i18n-6.18.1.tgz} +0 -0
  2. package/components/{tryghost-parse-email-address-6.17.2.tgz → tryghost-parse-email-address-6.18.1.tgz} +0 -0
  3. package/core/built/admin/assets/{PolarAngleAxis-B8JrAxf_.js → PolarAngleAxis-Dod0DwfL.js} +1 -1
  4. package/core/built/admin/assets/{_baseAssignValue-Bbalamaq.js → _baseAssignValue-DnkbkowM.js} +1 -1
  5. package/core/built/admin/assets/{a-large-small-CgWPeKpi.js → a-large-small-C5mgFBRg.js} +1 -1
  6. package/core/built/admin/assets/activitypub/activitypub.js +1 -1
  7. package/core/built/admin/assets/activitypub/{at-sign-Fsk3x72r.mjs → at-sign-BR2C2gdz.mjs} +2 -2
  8. package/core/built/admin/assets/activitypub/{avatar-flipboard-C10JfFS_.mjs → avatar-flipboard-CJV3KhLU.mjs} +2 -2
  9. package/core/built/admin/assets/activitypub/{bluesky-sharing-C1xeGSL6.mjs → bluesky-sharing-DY4KCGPZ.mjs} +4 -4
  10. package/core/built/admin/assets/activitypub/{copy-C1fElSkQ.mjs → copy-CBFwbAdE.mjs} +2 -2
  11. package/core/built/admin/assets/activitypub/{deleted-feed-item-Bun4tY2_.mjs → deleted-feed-item-DOm9GxTQ.mjs} +2 -2
  12. package/core/built/admin/assets/activitypub/{edit-profile-CYh00FZ7.mjs → edit-profile-CcbnZWkM.mjs} +3 -3
  13. package/core/built/admin/assets/activitypub/{feed-BxUqmcN9.mjs → feed-BHiZU0Q_.mjs} +4 -4
  14. package/core/built/admin/assets/activitypub/{hash-CNgwAx-U.mjs → hash-Csjloy5t.mjs} +2 -2
  15. package/core/built/admin/assets/activitypub/{inbox-DqNqII4a.mjs → inbox-DGKbBux9.mjs} +2 -2
  16. package/core/built/admin/assets/activitypub/{index-CONoLlDU.mjs → index-Bt_qFJNY.mjs} +2 -2
  17. package/core/built/admin/assets/activitypub/{index--Q6orQkb.mjs → index-CCYuVHjm.mjs} +21 -21
  18. package/core/built/admin/assets/activitypub/{index-jhjmoHwu.mjs → index-CTV39jCH.mjs} +2 -2
  19. package/core/built/admin/assets/activitypub/{index-Dvh9q3jy.mjs → index-ClNx7qzi.mjs} +4 -4
  20. package/core/built/admin/assets/activitypub/{index-C9pnotJK.mjs → index-D6Y6ywyl.mjs} +6 -6
  21. package/core/built/admin/assets/activitypub/{index-BueIufRq.mjs → index-YLvIz6pJ.mjs} +5 -5
  22. package/core/built/admin/assets/activitypub/{index-C3KJXzZE.mjs → index-tWANcGjH.mjs} +7 -7
  23. package/core/built/admin/assets/activitypub/{index-CJJXnqq1.mjs → index-vVqFEPIX.mjs} +3 -3
  24. package/core/built/admin/assets/activitypub/{moderation-CYhwUFi2.mjs → moderation-CUGqRfT1.mjs} +3 -3
  25. package/core/built/admin/assets/activitypub/{note-COVa8CMw.mjs → note-DMy39JHG.mjs} +4 -4
  26. package/core/built/admin/assets/activitypub/{reply-BHpKVBxx.mjs → reply-BboFcDsv.mjs} +2 -2
  27. package/core/built/admin/assets/activitypub/{separator-DP7q5sFH.mjs → separator-DBvIXQnV.mjs} +2 -2
  28. package/core/built/admin/assets/activitypub/{settings-3n7zo_3K.mjs → settings-D4qRhs9E.mjs} +3 -3
  29. package/core/built/admin/assets/activitypub/{step-1-BmUukywZ.mjs → step-1-C-ubwwIT.mjs} +3 -3
  30. package/core/built/admin/assets/activitypub/{step-2-C--I3xxp.mjs → step-2-Dsd2XY1_.mjs} +5 -5
  31. package/core/built/admin/assets/activitypub/{step-3-0Deh5N9c.mjs → step-3-BHz3ow0t.mjs} +7 -7
  32. package/core/built/admin/assets/activitypub/{tabs-D_vmoLBo.mjs → tabs-s_P4XeyF.mjs} +3 -3
  33. package/core/built/admin/assets/activitypub/{topic-filter-DJMrhH-c.mjs → topic-filter-CCf15CR-.mjs} +2 -2
  34. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +1 -1
  35. package/core/built/admin/assets/admin-x-settings/{code-editor-view-BzvYs1fs.mjs → code-editor-view-BWi3-ftq.mjs} +2 -2
  36. package/core/built/admin/assets/admin-x-settings/{index-DdMo_pvD.mjs → index-B3PtvbUw.mjs} +2 -2
  37. package/core/built/admin/assets/admin-x-settings/{index-DYEF3NiM.mjs → index-Did70N9h.mjs} +2 -2
  38. package/core/built/admin/assets/admin-x-settings/{index-BXChUyr6.mjs → index-JaMlaX9G.mjs} +30 -19
  39. package/core/built/admin/assets/admin-x-settings/{modals-Ck3AHDx3.mjs → modals-Bwrf9ptQ.mjs} +8060 -8067
  40. package/core/built/admin/assets/{at-sign-BottTo2X.js → at-sign-Bz-SU-S_.js} +1 -1
  41. package/core/built/admin/assets/{audience-CcGOt5Fb.js → audience-DmVdEmIe.js} +1 -1
  42. package/core/built/admin/assets/{avatar-flipboard-MxW2k124.js → avatar-flipboard-C7sxDVEM.js} +1 -1
  43. package/core/built/admin/assets/{bluesky-sharing-BBmDBlxr.js → bluesky-sharing-DJniSF6N.js} +1 -1
  44. package/core/built/admin/assets/{chart-pimL5Chk.js → chart-mYz3IJwm.js} +1 -1
  45. package/core/built/admin/assets/{chunk.524.9a8f5123fd3ecf3e4451.js → chunk.524.9d300778a63b42b0de62.js} +7 -7
  46. package/core/built/admin/assets/{chunk.582.41886d06af2895d571bd.js → chunk.582.2e363cd976d9bba998b9.js} +10 -10
  47. package/core/built/admin/assets/{code-editor-view-tCZOpc_h.js → code-editor-view-BIQWFJ01.js} +1 -1
  48. package/core/built/admin/assets/comments-B_-hdjc6.js +1 -0
  49. package/core/built/admin/assets/{copy-aC0WUVtq.js → copy-Bpq3uhnh.js} +1 -1
  50. package/core/built/admin/assets/{data-list-BOdyZGWI.js → data-list-BYNMbRIq.js} +1 -1
  51. package/core/built/admin/assets/{deleted-feed-item-BnUJ8bWP.js → deleted-feed-item-B_AwUKZy.js} +1 -1
  52. package/core/built/admin/assets/{edit-profile-CEgPhWMJ.js → edit-profile-DZo2ZcOu.js} +1 -1
  53. package/core/built/admin/assets/{empty-indicator-CR_9BHAH.js → empty-indicator-Bn5wG9-T.js} +1 -1
  54. package/core/built/admin/assets/{en-ajofhc7o.js → en-D4zIrMLN.js} +1 -1
  55. package/core/built/admin/assets/{feed-Ci-ODCHE.js → feed-Bgq4RarU.js} +1 -1
  56. package/core/built/admin/assets/{filters-CmJJOv3I.js → filters-CpVwu1Gk.js} +1 -1
  57. package/core/built/admin/assets/{gh-chart-hn7wEnik.js → gh-chart-Br7NXn_b.js} +1 -1
  58. package/core/built/admin/assets/{ghost-78b420974adc056bf1c9570275a46535.css → ghost-3c48d37b32f4fcd7eb15cfd739905a03.css} +1 -1
  59. package/core/built/admin/assets/{ghost-a9268074d0a9b02198594a05ecd7a626.js → ghost-98a3c5fd6235ebd344594f491cb6d17b.js} +241 -246
  60. package/core/built/admin/assets/{ghost-dark-12abca67e79c76fe6bfed6cb81ba2a53.css → ghost-dark-0100fe3ec7b4c3f6d8d043c904254a63.css} +1 -1
  61. package/core/built/admin/assets/{growth-CQceZQ1g.js → growth-CiPDnWxJ.js} +1 -1
  62. package/core/built/admin/assets/{hash-BcaWmkw5.js → hash-Bc1e38oH.js} +1 -1
  63. package/core/built/admin/assets/{inbox-D9hID-rO.js → inbox-BcZNBNhk.js} +1 -1
  64. package/core/built/admin/assets/{index-d_UH71zm.js → index-BFEx_ouC.js} +1 -1
  65. package/core/built/admin/assets/{index-kXF42nga.js → index-B_7QsC5T.js} +1 -1
  66. package/core/built/admin/assets/{index-CZ3lARQ2.js → index-Bm5ZeLtt.js} +1 -1
  67. package/core/built/admin/assets/{index-DJDgMk-T.js → index-BpcL7RmI.js} +2 -2
  68. package/core/built/admin/assets/{index-g4BWcHhl.js → index-Brg4tecQ.js} +1 -1
  69. package/core/built/admin/assets/{index-f5Lv9kLb.js → index-CMTzNTew.js} +29 -29
  70. package/core/built/admin/assets/{index-Cl2eSBwg.js → index-CTfJflJ2.js} +1 -1
  71. package/core/built/admin/assets/{index-BJS8H75W.js → index-Cn32t_gv.js} +1 -1
  72. package/core/built/admin/assets/{index-D8syEvSm.js → index-CsidX7Si.js} +1 -1
  73. package/core/built/admin/assets/{index-DO76vL0R.js → index-D212VvOz.js} +1 -1
  74. package/core/built/admin/assets/{index-B1rKIiOr.js → index-D67LJ_H4.js} +1 -1
  75. package/core/built/admin/assets/{index-BmK6ThYl.js → index-DU3bv9jz.js} +1 -1
  76. package/core/built/admin/assets/{index-DYM1MbXR.js → index-Dv13wKfX.js} +1 -1
  77. package/core/built/admin/assets/{index-Do091GLu.js → index-ETsoQLbU.js} +1 -1
  78. package/core/built/admin/assets/{index-rluYcfd-.js → index-ZYDRMgcT.js} +1 -1
  79. package/core/built/admin/assets/{koenig-lexical-D2XHvS2J.js → koenig-lexical-DZUWzN0P.js} +1 -1
  80. package/core/built/admin/assets/{kpi-card-Dga9rFZ5.js → kpi-card-DENsK2xK.js} +1 -1
  81. package/core/built/admin/assets/{kpis-BbnqD1za.js → kpis-CDrs2iS1.js} +1 -1
  82. package/core/built/admin/assets/{label-D8WHEOe0.js → label-BznQtEEo.js} +1 -1
  83. package/core/built/admin/assets/{links-dgwx7ko1.js → links-lpAC3T1p.js} +1 -1
  84. package/core/built/admin/assets/{lucide-react-BARFK2PZ.js → lucide-react-CBigk-fq.js} +1 -1
  85. package/core/built/admin/assets/{main-layout-CcjtCuF_.js → main-layout-_SYQRjIl.js} +1 -1
  86. package/core/built/admin/assets/{message-square-text-BxquqPhA.js → message-square-text-cV8O_qKq.js} +1 -1
  87. package/core/built/admin/assets/{minus-Ziy4LSYD.js → minus-NvnQTlW7.js} +1 -1
  88. package/core/built/admin/assets/{modals-fMQzR7L6.js → modals-XRSkribf.js} +3 -3
  89. package/core/built/admin/assets/{moderation-Ci9UNwiZ.js → moderation-BQp1GEWG.js} +1 -1
  90. package/core/built/admin/assets/{newsletter-DnrS7Fvi.js → newsletter-foM6KNNV.js} +1 -1
  91. package/core/built/admin/assets/{newsletters-D78pgnas.js → newsletters-BexdXUhn.js} +1 -1
  92. package/core/built/admin/assets/{note-BAfNweZN.js → note-DuaUGOeZ.js} +1 -1
  93. package/core/built/admin/assets/{overview-CWQPNOF0.js → overview-DgKBNqyc.js} +1 -1
  94. package/core/built/admin/assets/{pagemenu-DjSqq3Wu.js → pagemenu-CZyroidv.js} +1 -1
  95. package/core/built/admin/assets/{post-analytics-c8kRV0YB.js → post-analytics-DLK2SOSQ.js} +1 -1
  96. package/core/built/admin/assets/{post-analytics-context-CEqWyqgm.js → post-analytics-context-CF7C67-0.js} +1 -1
  97. package/core/built/admin/assets/{post-analytics-header-CpPPBAz4.js → post-analytics-header-7GtJCx0W.js} +1 -1
  98. package/core/built/admin/assets/{post-share-modal-Dln0kZhn.js → post-share-modal-D8R7DUZP.js} +1 -1
  99. package/core/built/admin/assets/posts/{comments-CHEqbZPq.mjs → comments-Bn_vn_sb.mjs} +434 -407
  100. package/core/built/admin/assets/posts/{dialog-VFomJYla.mjs → dialog-CKPcMrj7.mjs} +3 -3
  101. package/core/built/admin/assets/posts/{empty-indicator-CT9qQsMO.mjs → empty-indicator-kkrhP6K_.mjs} +2 -2
  102. package/core/built/admin/assets/posts/{filters-BXCfSKKS.mjs → filters-DHsxxy0F.mjs} +6 -6
  103. package/core/built/admin/assets/posts/{growth-xMitvyvt.mjs → growth-D7ACh-oR.mjs} +12 -12
  104. package/core/built/admin/assets/posts/{heading-DIulQwgD.mjs → heading-BrKVbOxA.mjs} +2 -2
  105. package/core/built/admin/assets/posts/{hooks-BiBMDYOA.mjs → hooks-BBEMuLiW.mjs} +3 -3
  106. package/core/built/admin/assets/posts/{index-uYyMBANY.mjs → index-C0rRguZx.mjs} +9 -9
  107. package/core/built/admin/assets/posts/{kpis-DK6j0CJA.mjs → kpis-1o524OIK.mjs} +10 -10
  108. package/core/built/admin/assets/posts/{links-CBiYZsyk.mjs → links-JpeATx1f.mjs} +4 -4
  109. package/core/built/admin/assets/posts/{loading-indicator-BPPeKpVb.mjs → loading-indicator-BHAmSf8j.mjs} +3 -3
  110. package/core/built/admin/assets/posts/{main-layout-f-PcNn-6.mjs → main-layout-BTItAOQE.mjs} +2 -2
  111. package/core/built/admin/assets/posts/{newsletter-CSOyAfvO.mjs → newsletter-CQdsCEHv.mjs} +13 -13
  112. package/core/built/admin/assets/posts/{overview-Dve3Uxt4.mjs → overview-sYh4JN1D.mjs} +15 -15
  113. package/core/built/admin/assets/posts/{post-analytics-CPKxH7yg.mjs → post-analytics-DtLt2SWx.mjs} +6 -6
  114. package/core/built/admin/assets/posts/{post-analytics-context-B5zQoflZ.mjs → post-analytics-context-CB1yM9fk.mjs} +4 -4
  115. package/core/built/admin/assets/posts/{post-analytics-header-BBxuaptK.mjs → post-analytics-header-BYaWuL9W.mjs} +9 -9
  116. package/core/built/admin/assets/posts/{post-share-modal-D_iMXvnI.mjs → post-share-modal-V0HTOBaf.mjs} +4 -4
  117. package/core/built/admin/assets/posts/{posts-DW6ne0od.mjs → posts-DeQT3knv.mjs} +2 -2
  118. package/core/built/admin/assets/posts/posts.js +1 -1
  119. package/core/built/admin/assets/posts/{search-CVlPqGMA.mjs → search-CLjC37AT.mjs} +2 -2
  120. package/core/built/admin/assets/posts/{separator-YrJkDGVP.mjs → separator-KNoTIaJx.mjs} +5 -5
  121. package/core/built/admin/assets/posts/{sheet-T5GWxxrn.mjs → sheet-DBg_SCDt.mjs} +3 -3
  122. package/core/built/admin/assets/posts/{skeleton-qnvlkDy-.mjs → skeleton-DvsoolWu.mjs} +3 -3
  123. package/core/built/admin/assets/posts/{source-icon-DBU0eZwS.mjs → source-icon-OmRaTU2G.mjs} +3 -3
  124. package/core/built/admin/assets/posts/{stats-udwJVZP8.mjs → stats-BC2DzntY.mjs} +4 -4
  125. package/core/built/admin/assets/posts/{table-Cu0BZfBU.mjs → table-CxX9OKAj.mjs} +2 -2
  126. package/core/built/admin/assets/posts/{tabs-B-RaoD3I.mjs → tabs-B1jw7cBi.mjs} +10 -10
  127. package/core/built/admin/assets/posts/{tags-DSDGzArh.mjs → tags-BBilTo1a.mjs} +11 -11
  128. package/core/built/admin/assets/posts/{tags-DByfrDKO.mjs → tags-BitrLT_j.mjs} +2 -2
  129. package/core/built/admin/assets/posts/{use-infinite-virtual-scroll-B7dD2nWW.mjs → use-infinite-virtual-scroll-DaijA1ao.mjs} +3 -3
  130. package/core/built/admin/assets/posts/{web-B9wvEtJ_.mjs → web-CKmyC4Xj.mjs} +15 -15
  131. package/core/built/admin/assets/{posts-C1Eh5Ypq.js → posts-CL9UDYoW.js} +1 -1
  132. package/core/built/admin/assets/{repeat-CN4fiG5H.js → repeat-DgH39UKE.js} +1 -1
  133. package/core/built/admin/assets/{reply-BfWgvH7y.js → reply-DAaNxiy8.js} +1 -1
  134. package/core/built/admin/assets/{select-B_0NBkLz.js → select-Cor2wFXT.js} +1 -1
  135. package/core/built/admin/assets/{settings-BqaVcJee.js → settings-BeumESEN.js} +5 -5
  136. package/core/built/admin/assets/{settings-Dk9E7Fqv.js → settings-xRx917Gj.js} +1 -1
  137. package/core/built/admin/assets/{sort-button-C2M35dLh.js → sort-button-BNW3i4Lb.js} +1 -1
  138. package/core/built/admin/assets/{source-icon-DkGi13e_.js → source-icon-DvDuzw73.js} +1 -1
  139. package/core/built/admin/assets/{sprout-BKiwQsGn.js → sprout-C3cc0c-K.js} +1 -1
  140. package/core/built/admin/assets/{square-BEpRc_fY.js → square-tZp0_n7e.js} +1 -1
  141. package/core/built/admin/assets/stats/{audience-Dfo7BmNu.mjs → audience-BWqU7WWT.mjs} +3 -3
  142. package/core/built/admin/assets/stats/{index-DUWoiDE_.mjs → index-CUuQaROI.mjs} +5 -5
  143. package/core/built/admin/assets/stats/{index-Yo58temM.mjs → index-CcCyLMxL.mjs} +7 -7
  144. package/core/built/admin/assets/stats/{index-h0rsyxDz.mjs → index-D5mlMG4l.mjs} +8 -8
  145. package/core/built/admin/assets/stats/{index-_vL3Zubi.mjs → index-DXU2rE9t.mjs} +5 -5
  146. package/core/built/admin/assets/stats/{index-Banm1wtA.mjs → index-K7ASx7EG.mjs} +6 -6
  147. package/core/built/admin/assets/stats/{sort-button-i6PS-TUn.mjs → sort-button-CELUx6Zp.mjs} +3 -3
  148. package/core/built/admin/assets/stats/{stats-CjepXEWS.mjs → stats-d_u_in4l.mjs} +2 -2
  149. package/core/built/admin/assets/stats/stats.js +2 -2
  150. package/core/built/admin/assets/stats/{tabs-Q20S1oup.mjs → tabs-3wLZsy0v.mjs} +3 -3
  151. package/core/built/admin/assets/stats/{url-helpers-DrGoeEH1.mjs → url-helpers-Drq3xg0l.mjs} +4 -4
  152. package/core/built/admin/assets/stats/{use-growth-stats-BnffY2W3.mjs → use-growth-stats-28Sr42va.mjs} +3 -3
  153. package/core/built/admin/assets/{stats-CRgy_cWm.js → stats-2Jelnn-Q.js} +1 -1
  154. package/core/built/admin/assets/{stats-view-CTQ4qO3S.js → stats-view-CESy8ELH.js} +1 -1
  155. package/core/built/admin/assets/{step-1-CNbdmB8C.js → step-1-DrqdolAh.js} +1 -1
  156. package/core/built/admin/assets/{step-2-Ddrl2uae.js → step-2-DmEpKck5.js} +1 -1
  157. package/core/built/admin/assets/{step-3-CbqkEtwh.js → step-3-Bus-0o0n.js} +1 -1
  158. package/core/built/admin/assets/{table-Dw9xFlFd.js → table-BQUcKHfm.js} +1 -1
  159. package/core/built/admin/assets/{tabs-DCHagOip.js → tabs-BmdL0X4U.js} +1 -1
  160. package/core/built/admin/assets/{tags-DV2SSdMK.js → tags-CLxXZlOO.js} +1 -1
  161. package/core/built/admin/assets/{tags-BsDlfsPN.js → tags-EchqlZUJ.js} +1 -1
  162. package/core/built/admin/assets/{tiers-tc5tafWV.js → tiers-nCGyTly9.js} +1 -1
  163. package/core/built/admin/assets/{toggle-group-DS-ZVhCr.js → toggle-group-CM5uf7J1.js} +1 -1
  164. package/core/built/admin/assets/{topic-filter-BfoEqHRD.js → topic-filter-LTRvZ8aU.js} +1 -1
  165. package/core/built/admin/assets/{trash-CpnxYen-.js → trash-u5BxolyH.js} +1 -1
  166. package/core/built/admin/assets/{url-helpers-wKPPeCtD.js → url-helpers-D41fEt51.js} +1 -1
  167. package/core/built/admin/assets/{use-growth-stats-CbHIETF_.js → use-growth-stats-BJ0O9ewi.js} +1 -1
  168. package/core/built/admin/assets/{use-infinite-virtual-scroll-DU1EUhyj.js → use-infinite-virtual-scroll-APZWciOk.js} +1 -1
  169. package/core/built/admin/assets/{use-simple-pagination-X6kmRz2u.js → use-simple-pagination-DVRHeaAR.js} +1 -1
  170. package/core/built/admin/assets/{user-round-check-DYkrjHxF.js → user-round-check-B6j98D6d.js} +1 -1
  171. package/core/built/admin/assets/{wallet-cards-CSdWOmJE.js → wallet-cards-KmOh29LP.js} +1 -1
  172. package/core/built/admin/assets/{web-C39IKnvl.js → web-Cclotbnz.js} +1 -1
  173. package/core/built/admin/index.html +5 -5
  174. package/core/frontend/meta/asset-url.js +126 -15
  175. package/core/frontend/services/asset-hash/index.js +73 -0
  176. package/core/frontend/services/theme-engine/active.js +4 -2
  177. package/core/server/api/endpoints/utils/serializers/output/utils/post-gating.js +6 -0
  178. package/core/server/data/migrations/versions/6.17/2026-02-04-10-42-20-offers-nullable-product-id.js +11 -0
  179. package/core/server/data/migrations/versions/6.18/2026-02-04-10-42-20-offers-nullable-product-id.js +4 -0
  180. package/core/server/data/schema/schema.js +1 -1
  181. package/core/server/data/tinybird/datasources/_mv_daily_pages.datasource +13 -0
  182. package/core/server/data/tinybird/endpoints/api_top_pages_v3.pipe +113 -0
  183. package/core/server/data/tinybird/pipes/mv_daily_pages.pipe +23 -0
  184. package/core/server/data/tinybird/scripts/benchmark-top-pages.sh +186 -0
  185. package/core/server/data/tinybird/scripts/compare-top-pages.sh +262 -0
  186. package/core/server/data/tinybird/tests/api_top_pages_v3.yaml +112 -0
  187. package/core/server/models/automated-email.js +34 -0
  188. package/core/server/services/koenig/node-renderers/transistor-renderer.js +3 -8
  189. package/core/server/services/lib/member-signup-contexts.js +23 -0
  190. package/core/server/services/lib/member-signup-contexts.ts +22 -0
  191. package/core/server/services/member-welcome-emails/member-welcome-email-renderer.js +13 -7
  192. package/core/server/services/member-welcome-emails/service.js +84 -15
  193. package/core/server/services/members/members-api/controllers/router-controller.js +14 -0
  194. package/core/server/services/members/members-api/repositories/member-repository.js +1 -1
  195. package/core/server/services/members/members-api/services/payments-service.js +5 -0
  196. package/core/server/services/oembed/oembed-service.js +1 -1
  197. package/core/server/services/offers/application/offer-mapper.js +6 -7
  198. package/core/server/services/offers/application/offers-api.js +3 -3
  199. package/core/server/services/offers/domain/errors/index.js +2 -0
  200. package/core/server/services/offers/domain/models/offer.js +15 -3
  201. package/core/server/services/offers/offer-bookshelf-repository.js +4 -5
  202. package/core/server/services/stats/utils/tinybird.js +5 -3
  203. package/core/server/services/stripe/services/webhook/checkout-session-event-service.js +17 -1
  204. package/core/server/services/stripe/stripe-service.js +8 -0
  205. package/core/shared/config/defaults.json +6 -0
  206. package/core/shared/labs.js +3 -3
  207. package/core/shared/url-utils.js +2 -1
  208. package/package.json +6 -6
  209. package/tsconfig.tsbuildinfo +1 -1
  210. package/yarn.lock +23 -2
  211. package/core/built/admin/assets/comments-OXqhcC0t.js +0 -1
@@ -0,0 +1,22 @@
1
+ // Signup context describes the sign-in state when the Stripe checkout session is created.
2
+ // NEEDS_MAGIC_LINK_EMAIL: No guaranteed sign-in path exists yet (custom/direct checkout paths).
3
+ // HAS_PRECHECKOUT_MAGIC_LINK: Ghost generated a signup magic-link before Stripe (standard Portal flow).
4
+ // ALREADY_AUTHENTICATED: Request came from a signed-in member identity (for example, opening a paid signup link directly).
5
+ export const SIGNUP_CONTEXTS = {
6
+ NEEDS_MAGIC_LINK_EMAIL: 'needs_magic_link_email',
7
+ HAS_PRECHECKOUT_MAGIC_LINK: 'has_precheckout_magic_link',
8
+ ALREADY_AUTHENTICATED: 'already_authenticated'
9
+ } as const;
10
+
11
+ export type SignupContext = typeof SIGNUP_CONTEXTS[keyof typeof SIGNUP_CONTEXTS];
12
+
13
+ /**
14
+ * Signup-paid email can be skipped when welcome email is active only if
15
+ * checkout already has a reliable sign-in path.
16
+ *
17
+ * - HAS_PRECHECKOUT_MAGIC_LINK: standard Portal flow generated signup link before Stripe
18
+ * - ALREADY_AUTHENTICATED: checkout request came from an already signed-in member
19
+ */
20
+ export function canWelcomeEmailReplaceSignupPaidEmail(signupContext?: SignupContext) {
21
+ return signupContext === SIGNUP_CONTEXTS.HAS_PRECHECKOUT_MAGIC_LINK || signupContext === SIGNUP_CONTEXTS.ALREADY_AUTHENTICATED;
22
+ }
@@ -53,14 +53,12 @@ class MemberWelcomeEmailRenderer {
53
53
  * Applies replacement tokens to a string
54
54
  * Supports fallback values: {first_name, "friend"} renders "friend" if name is empty
55
55
  * @param {Object} options
56
+ * @param {{id: string, getValue: () => string|undefined}[]} options.definitions - Replacement token definitions
56
57
  * @param {string} options.text - The text to process (content body or subject line)
57
- * @param {Object} options.member - Member data
58
- * @param {Object} options.siteSettings - Site settings
59
58
  * @param {boolean} [options.escapeHtml=false] - Whether to HTML-escape replaced values
60
59
  * @returns {string}
61
60
  */
62
- #applyReplacements({text, member, siteSettings, escapeHtml = false}) {
63
- const definitions = this.#buildReplacementDefinitions({member, siteSettings});
61
+ #applyReplacements({definitions, text, escapeHtml = false}) {
64
62
  let processed = wrapReplacementStrings(text);
65
63
 
66
64
  processed = processed.replace(REPLACEMENT_REGEX, (match, property, fallback) => {
@@ -96,8 +94,17 @@ class MemberWelcomeEmailRenderer {
96
94
  });
97
95
  }
98
96
 
99
- const contentWithReplacements = this.#applyReplacements({text: content, member, siteSettings, escapeHtml: true});
100
- const subjectWithReplacements = this.#applyReplacements({text: subject, member, siteSettings, escapeHtml: false});
97
+ const definitions = this.#buildReplacementDefinitions({member, siteSettings});
98
+
99
+ // Remove <code> wrappers around replacement strings (Lexical treats curly braces as inline code)
100
+ const tokenIds = definitions.map(d => d.id).join('|');
101
+ content = content.replace(
102
+ new RegExp(`<code>(\\{(?:${tokenIds})(?:\\s*,?\\s*"[^"]*")?\\})<\\/code>`, 'g'),
103
+ '$1'
104
+ );
105
+
106
+ const contentWithReplacements = this.#applyReplacements({definitions, text: content, escapeHtml: true});
107
+ const subjectWithReplacements = this.#applyReplacements({definitions, text: subject, escapeHtml: false});
101
108
 
102
109
  const managePreferencesUrl = new URL('#/portal/account/newsletters', siteSettings.url).href;
103
110
  const year = new Date().getFullYear();
@@ -124,4 +131,3 @@ class MemberWelcomeEmailRenderer {
124
131
  }
125
132
 
126
133
  module.exports = MemberWelcomeEmailRenderer;
127
-
@@ -2,11 +2,12 @@ const logging = require('@tryghost/logging');
2
2
  const errors = require('@tryghost/errors');
3
3
  const urlUtils = require('../../../shared/url-utils');
4
4
  const settingsCache = require('../../../shared/settings-cache');
5
- const config = require('../../../shared/config');
6
5
  const emailAddressService = require('../email-address');
6
+ const settingsHelpers = require('../settings-helpers');
7
+ const EmailAddressParser = require('../email-address/email-address-parser');
7
8
  const mail = require('../mail');
8
9
  // @ts-expect-error type checker has trouble with the dynamic exporting in models
9
- const {AutomatedEmail} = require('../../models');
10
+ const {AutomatedEmail, Newsletter} = require('../../models');
10
11
  const MemberWelcomeEmailRenderer = require('./member-welcome-email-renderer');
11
12
  const {MEMBER_WELCOME_EMAIL_LOG_KEY, MEMBER_WELCOME_EMAIL_SLUGS, MESSAGES} = require('./constants');
12
13
 
@@ -14,6 +15,7 @@ class MemberWelcomeEmailService {
14
15
  #mailer;
15
16
  #renderer;
16
17
  #memberWelcomeEmails = {free: null, paid: null};
18
+ #defaultNewsletterSenderOptions = null;
17
19
 
18
20
  constructor() {
19
21
  emailAddressService.init();
@@ -29,7 +31,71 @@ class MemberWelcomeEmailService {
29
31
  };
30
32
  }
31
33
 
34
+ async #getDefaultNewsletterSenderOptions() {
35
+ const newsletter = await Newsletter.getDefaultNewsletter();
36
+ if (!newsletter) {
37
+ return {};
38
+ }
39
+
40
+ let senderName = settingsCache.get('title') || '';
41
+ if (newsletter.get('sender_name')) {
42
+ senderName = newsletter.get('sender_name');
43
+ }
44
+
45
+ let fromAddress = settingsHelpers.getNoReplyAddress();
46
+ if (newsletter.get('sender_email')) {
47
+ fromAddress = newsletter.get('sender_email');
48
+ }
49
+
50
+ const fromAddresses = emailAddressService.service.getAddress({
51
+ from: {
52
+ address: fromAddress,
53
+ name: senderName || undefined
54
+ }
55
+ });
56
+
57
+ const from = EmailAddressParser.stringify(fromAddresses.from);
58
+ const replyToSetting = newsletter.get('sender_reply_to');
59
+ let replyTo = null;
60
+
61
+ if (replyToSetting === 'support') {
62
+ replyTo = settingsHelpers.getMembersSupportAddress();
63
+ } else if (replyToSetting === 'newsletter' && !emailAddressService.service.managedEmailEnabled) {
64
+ replyTo = from;
65
+ } else {
66
+ const addresses = emailAddressService.service.getAddress({
67
+ from: {
68
+ address: fromAddress,
69
+ name: senderName || undefined
70
+ },
71
+ replyTo: replyToSetting === 'newsletter' ? undefined : {address: replyToSetting}
72
+ });
73
+
74
+ if (addresses.replyTo) {
75
+ replyTo = EmailAddressParser.stringify(addresses.replyTo);
76
+ }
77
+ }
78
+
79
+ return {
80
+ from,
81
+ ...(replyTo ? {
82
+ replyTo
83
+ } : {})
84
+ };
85
+ }
86
+
87
+ async #getSenderOptions() {
88
+ if (this.#defaultNewsletterSenderOptions) {
89
+ return this.#defaultNewsletterSenderOptions;
90
+ }
91
+
92
+ this.#defaultNewsletterSenderOptions = await this.#getDefaultNewsletterSenderOptions();
93
+ return this.#defaultNewsletterSenderOptions;
94
+ }
95
+
32
96
  async loadMemberWelcomeEmails() {
97
+ this.#defaultNewsletterSenderOptions = await this.#getDefaultNewsletterSenderOptions();
98
+
33
99
  for (const [memberStatus, slug] of Object.entries(MEMBER_WELCOME_EMAIL_SLUGS)) {
34
100
  const row = await AutomatedEmail.findOne({slug});
35
101
 
@@ -50,8 +116,14 @@ class MemberWelcomeEmailService {
50
116
  }
51
117
 
52
118
  async send({member, memberStatus}) {
119
+ if (!member.email) {
120
+ throw new errors.IncorrectUsageError({
121
+ message: MESSAGES.MISSING_RECIPIENT_EMAIL
122
+ });
123
+ }
124
+
53
125
  const name = member?.name ? `${member.name} at ` : '';
54
- logging.info(`${MEMBER_WELCOME_EMAIL_LOG_KEY} Sending welcome email to ${name}${member?.email}`);
126
+ logging.info(`${MEMBER_WELCOME_EMAIL_LOG_KEY} Sending welcome email to ${name}${member.email}`);
55
127
 
56
128
  const memberWelcomeEmail = this.#memberWelcomeEmails[memberStatus];
57
129
 
@@ -77,21 +149,15 @@ class MemberWelcomeEmailService {
77
149
  siteSettings: this.#getSiteSettings()
78
150
  });
79
151
 
80
- const testInbox = config.get('memberWelcomeEmailTestInbox');
81
- const toEmail = testInbox || member.email;
82
-
83
- if (!toEmail) {
84
- throw new errors.IncorrectUsageError({
85
- message: MESSAGES.MISSING_RECIPIENT_EMAIL
86
- });
87
- }
152
+ const senderOptions = await this.#getSenderOptions();
88
153
 
89
154
  await this.#mailer.send({
90
- to: toEmail,
155
+ to: member.email,
91
156
  subject,
92
157
  html,
93
158
  text,
94
- forceTextContent: true
159
+ forceTextContent: true,
160
+ ...senderOptions
95
161
  });
96
162
  }
97
163
 
@@ -140,12 +206,16 @@ class MemberWelcomeEmailService {
140
206
  siteSettings: this.#getSiteSettings()
141
207
  });
142
208
 
209
+ // Test sends should always reflect the latest newsletter sender settings.
210
+ const senderOptions = await this.#getDefaultNewsletterSenderOptions();
211
+
143
212
  await this.#mailer.send({
144
213
  to: email,
145
214
  subject: `[Test] ${renderedSubject}`,
146
215
  html,
147
216
  text,
148
- forceTextContent: true
217
+ forceTextContent: true,
218
+ ...senderOptions
149
219
  });
150
220
  }
151
221
  }
@@ -160,4 +230,3 @@ class MemberWelcomeEmailServiceWrapper {
160
230
  }
161
231
 
162
232
  module.exports = new MemberWelcomeEmailServiceWrapper();
163
-
@@ -7,6 +7,8 @@ const errors = require('@tryghost/errors');
7
7
  const {isEmail} = require('@tryghost/validator');
8
8
  const normalizeEmail = require('../utils/normalize-email');
9
9
  const {getInboxLinks} = require('../../../../lib/get-inbox-links');
10
+ const {SIGNUP_CONTEXTS} = require('../../../lib/member-signup-contexts');
11
+ /** @typedef {import('../../../lib/member-signup-contexts').SignupContext} SignupContext */
10
12
 
11
13
  const messages = {
12
14
  emailRequired: 'Email is required.',
@@ -392,6 +394,12 @@ module.exports = class RouterController {
392
394
  });
393
395
  }
394
396
 
397
+ if (!offer.tier) {
398
+ throw new BadRequestError({
399
+ message: 'Offer does not have a tier'
400
+ });
401
+ }
402
+
395
403
  tier = await this._tiersService.api.read(offer.tier.id);
396
404
  cadence = offer.cadence;
397
405
  } else if (tierId) {
@@ -469,6 +477,8 @@ module.exports = class RouterController {
469
477
  }
470
478
 
471
479
  const member = options.member;
480
+ /** @type {SignupContext} */
481
+ let ghostSignupContext = (options.isAuthenticated && member) ? SIGNUP_CONTEXTS.ALREADY_AUTHENTICATED : SIGNUP_CONTEXTS.NEEDS_MAGIC_LINK_EMAIL;
472
482
 
473
483
  if (!member && options.email) {
474
484
  // Create a signup link if there is no member with this email address
@@ -485,6 +495,7 @@ module.exports = class RouterController {
485
495
  // Redirect to the original success url after sign up
486
496
  referrer: options.successUrl
487
497
  });
498
+ ghostSignupContext = SIGNUP_CONTEXTS.HAS_PRECHECKOUT_MAGIC_LINK;
488
499
  }
489
500
 
490
501
  if (member) {
@@ -509,6 +520,9 @@ module.exports = class RouterController {
509
520
  }
510
521
  }
511
522
 
523
+ // Set by server to distinguish between checkout flows in Stripe webhooks.
524
+ options.metadata.ghostSignupContext = ghostSignupContext;
525
+
512
526
  try {
513
527
  const paymentLink = await this._paymentsService.getPaymentLink(options);
514
528
 
@@ -1769,7 +1769,7 @@ module.exports = class MemberRepository {
1769
1769
  });
1770
1770
  }
1771
1771
 
1772
- if (offer.tier.id !== tierId) {
1772
+ if (offer.tier && offer.tier.id !== tierId) {
1773
1773
  throw new errors.BadRequestError({
1774
1774
  message: tpl(messages.offerTierMismatch)
1775
1775
  });
@@ -72,6 +72,11 @@ class PaymentsService {
72
72
  let coupon = null;
73
73
  let trialDays = null;
74
74
  if (offer) {
75
+ if (!offer.tier) {
76
+ throw new BadRequestError({
77
+ message: 'Offer does not have a tier'
78
+ });
79
+ }
75
80
  if (!tier.id.equals(offer.tier.id)) {
76
81
  throw new BadRequestError({
77
82
  message: 'This Offer is not valid for the Tier'
@@ -10,7 +10,7 @@ const iconv = require('iconv-lite');
10
10
  const path = require('path');
11
11
 
12
12
  // Some sites block non-standard user agents so we need to mimic a typical browser
13
- const USER_AGENT = 'Mozilla/5.0 (compatible; Ghost/5.0; +https://ghost.org/)';
13
+ const USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36';
14
14
 
15
15
  const messages = {
16
16
  noUrlProvided: 'No url provided.',
@@ -26,9 +26,9 @@
26
26
  * @prop {number} redemption_count
27
27
  * @prop {'signup'|'retention'} redemption_type
28
28
  *
29
- * @prop {object} tier
30
- * @prop {string} tier.id
31
- * @prop {string} tier.name
29
+ * @prop {object|null} tier
30
+ * @prop {string} [tier.id]
31
+ * @prop {string} [tier.name]
32
32
  * @prop {string} created_at
33
33
  * @prop {string|null} last_redeemed
34
34
  */
@@ -55,10 +55,9 @@ class OfferMapper {
55
55
  status: offer.status.value,
56
56
  redemption_count: offer.redemptionCount,
57
57
  redemption_type: offer.redemptionType.value,
58
- tier: {
59
- id: offer.tier.id,
60
- name: offer.tier.name
61
- },
58
+ tier: offer.tier
59
+ ? {id: offer.tier.id, name: offer.tier.name}
60
+ : null,
62
61
  created_at: offer.createdAt,
63
62
  last_redeemed: offer.lastRedeemed
64
63
  };
@@ -165,12 +165,12 @@ class OffersAPI {
165
165
  return [];
166
166
  }
167
167
 
168
- // Filter by tier and cadence
169
- let available = allOffers.filter(offer => offer.tier.id === tierId && offer.cadence.value === cadence);
168
+ // Filter by tier and cadence - Null-tier offers (retention) match any tier with the correct cadence
169
+ let available = allOffers.filter(offer => (offer.tier === null || offer.tier.id === tierId) && offer.cadence.value === cadence);
170
170
  debug(`listOffersAvailableToSubscription: ${available.length} offers match tier and cadence`);
171
171
 
172
172
  if (available.length === 0) {
173
- const tierIds = [...new Set(allOffers.map(o => o.tier.id))];
173
+ const tierIds = [...new Set(allOffers.filter(o => o.tier !== null).map(o => o.tier.id))];
174
174
  const cadences = [...new Set(allOffers.map(o => o.cadence.value))];
175
175
  debug(`listOffersAvailableToSubscription: no offers match - available tiers: [${tierIds.join(', ')}], available cadences: [${cadences.join(', ')}]`);
176
176
 
@@ -18,6 +18,7 @@ class InvalidOfferCode extends InvalidPropError {}
18
18
  class InvalidOfferType extends InvalidPropError {}
19
19
  class InvalidOfferAmount extends InvalidPropError {}
20
20
  class InvalidOfferCurrency extends InvalidPropError {}
21
+ class InvalidOfferTier extends InvalidPropError {}
21
22
  class InvalidOfferTierName extends InvalidPropError {}
22
23
  class InvalidOfferCadence extends InvalidPropError {}
23
24
  class InvalidOfferDuration extends InvalidPropError {}
@@ -36,6 +37,7 @@ module.exports = {
36
37
  InvalidOfferCurrency,
37
38
  InvalidOfferCadence,
38
39
  InvalidOfferDuration,
40
+ InvalidOfferTier,
39
41
  InvalidOfferTierName,
40
42
  InvalidOfferCoupon,
41
43
  InvalidOfferStatus,
@@ -31,7 +31,7 @@ const StripeCoupon = require('./stripe-coupon');
31
31
  * @prop {OfferCurrency} [currency]
32
32
  * @prop {OfferStatus} status
33
33
  * @prop {string|null} [stripeCouponId]
34
- * @prop {OfferTier} tier
34
+ * @prop {OfferTier|null} tier
35
35
  * @prop {number} redemptionCount
36
36
  * @prop {OfferRedemptionType} redemptionType
37
37
  * @prop {string} createdAt
@@ -55,7 +55,7 @@ const StripeCoupon = require('./stripe-coupon');
55
55
  * @prop {string} [stripe_coupon_id]
56
56
  * @prop {number} [redemptionCount]
57
57
  * @prop {string} [redemption_type]
58
- * @prop {TierProps|OfferTier} tier
58
+ * @prop {TierProps|OfferTier|null} tier
59
59
  * @prop {Date} [created_at]
60
60
  * @prop {Date} [last_redeemed]
61
61
  */
@@ -356,8 +356,20 @@ class Offer {
356
356
  }
357
357
  }
358
358
 
359
- const tier = OfferTier.create(data.tier);
360
359
  const redemptionType = OfferRedemptionType.create(data.redemption_type || 'signup');
360
+ const tier = data.tier ? OfferTier.create(data.tier) : null;
361
+
362
+ if (redemptionType.value === 'signup' && !tier) {
363
+ throw new errors.InvalidOfferTier({
364
+ message: 'Signup offers must be associated with a tier'
365
+ });
366
+ }
367
+
368
+ if (redemptionType.value === 'retention' && tier) {
369
+ throw new errors.InvalidOfferTier({
370
+ message: 'Retention offers cannot be associated with a specific tier'
371
+ });
372
+ }
361
373
 
362
374
  return new Offer({
363
375
  id,
@@ -125,10 +125,9 @@ class OfferBookshelfRepository {
125
125
  redemptionCount: count,
126
126
  redemption_type: json.redemption_type,
127
127
  status: json.active ? 'active' : 'archived',
128
- tier: {
129
- id: json.product.id,
130
- name: json.product.name
131
- },
128
+ tier: json.product && json.product.id
129
+ ? {id: json.product.id, name: json.product.name}
130
+ : null,
132
131
  created_at: json.created_at,
133
132
  last_redeemed: lastRedeemedObject.length > 0 ? lastRedeemedObject[0].created_at : null
134
133
  }, null);
@@ -225,7 +224,7 @@ class OfferBookshelfRepository {
225
224
  discount_type: offer.type.value === 'fixed' ? 'amount' : offer.type.value,
226
225
  discount_amount: offer.amount.value,
227
226
  interval: offer.cadence.value,
228
- product_id: offer.tier.id,
227
+ product_id: offer.tier ? offer.tier.id : null,
229
228
  duration: offer.duration.value.type,
230
229
  duration_in_months: offer.duration.value.type === 'repeating' ? offer.duration.value.months : null,
231
230
  currency: offer.currency ? offer.currency.value : null,
@@ -19,6 +19,7 @@ const create = ({config, request, settingsCache, tinybirdService}) => {
19
19
  * @param {string} [options.timezone] - Timezone for the query
20
20
  * @param {string} [options.memberStatus] - Member status filter (defaults to 'all')
21
21
  * @param {string} [options.postType] - Post type filter
22
+ * @param {string} [options.version] - Version override (e.g., 'v3') - bypasses config version
22
23
  * @returns {Object} Object with URL and request options
23
24
  */
24
25
  const buildRequest = (pipeName, options = {}) => {
@@ -32,9 +33,10 @@ const create = ({config, request, settingsCache, tinybirdService}) => {
32
33
  const tokenData = tinybirdService.getToken();
33
34
  const token = tokenData?.token;
34
35
 
35
- // Use version from config if provided for constructing the URL
36
+ // Use version from options if provided, otherwise fall back to config
36
37
  // Pattern: api_kpis -> api_kpis_v2 (single underscore + version)
37
- const version = statsConfig?.version;
38
+ // Pass empty string to force unversioned endpoint
39
+ const version = options.version !== undefined ? options.version : statsConfig?.version;
38
40
  const pipeUrl = version ?
39
41
  `/v0/pipes/${pipeName}_${version}.json` :
40
42
  `/v0/pipes/${pipeName}.json`;
@@ -64,7 +66,7 @@ const create = ({config, request, settingsCache, tinybirdService}) => {
64
66
  }
65
67
  // Add any other options that might be needed
66
68
  Object.entries(options).forEach(([key, value]) => {
67
- if (!['dateFrom', 'dateTo', 'timezone', 'memberStatus', 'postType'].includes(key) && value !== undefined && value !== null) {
69
+ if (!['dateFrom', 'dateTo', 'timezone', 'memberStatus', 'postType', 'version'].includes(key) && value !== undefined && value !== null) {
68
70
  // Convert camelCase to snake_case for Tinybird API
69
71
  const snakeKey = key.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
70
72
  // Handle arrays by converting them to comma-separated strings for Tinybird
@@ -1,6 +1,10 @@
1
1
  const _ = require('lodash');
2
2
  const errors = require('@tryghost/errors');
3
3
  const logging = require('@tryghost/logging');
4
+ const {
5
+ canWelcomeEmailReplaceSignupPaidEmail
6
+ } = require('../../../lib/member-signup-contexts');
7
+ /** @typedef {import('../../../lib/member-signup-contexts').SignupContext} SignupContext */
4
8
 
5
9
  /**
6
10
  * Handles `checkout.session.completed` webhook events
@@ -24,6 +28,7 @@ module.exports = class CheckoutSessionEventService {
24
28
  * @param {object} deps.donationRepository
25
29
  * @param {object} deps.staffServiceEmails
26
30
  * @param {function} deps.sendSignupEmail
31
+ * @param {function} deps.isPaidWelcomeEmailActive
27
32
  */
28
33
  constructor(deps) {
29
34
  this.api = deps.api;
@@ -260,7 +265,18 @@ module.exports = class CheckoutSessionEventService {
260
265
  }
261
266
 
262
267
  if (checkoutType !== 'upgrade') {
263
- this.deps.sendSignupEmail(customer.email);
268
+ const ghostSignupContext = /** @type {SignupContext | undefined} */ (session.metadata?.ghostSignupContext);
269
+ const shouldSkipSignupEmailWhenWelcomeEmailActive = canWelcomeEmailReplaceSignupPaidEmail(ghostSignupContext);
270
+
271
+ if (shouldSkipSignupEmailWhenWelcomeEmailActive) {
272
+ const isPaidWelcomeEmailActive = await this.deps.isPaidWelcomeEmailActive();
273
+ if (!isPaidWelcomeEmailActive) {
274
+ this.deps.sendSignupEmail(customer.email);
275
+ }
276
+ } else {
277
+ // Direct checkout flows do not have a pre-checkout sign-in path.
278
+ this.deps.sendSignupEmail(customer.email);
279
+ }
264
280
  }
265
281
  }
266
282
  };
@@ -8,6 +8,7 @@ const {StripeLiveEnabledEvent, StripeLiveDisabledEvent} = require('./events');
8
8
  const SubscriptionEventService = require('./services/webhook/subscription-event-service');
9
9
  const InvoiceEventService = require('./services/webhook/invoice-event-service');
10
10
  const CheckoutSessionEventService = require('./services/webhook/checkout-session-event-service');
11
+ const memberWelcomeEmailService = require('../member-welcome-emails/service');
11
12
 
12
13
  /**
13
14
  * @typedef {object} IStripeServiceConfig
@@ -120,6 +121,13 @@ module.exports = class StripeService {
120
121
  },
121
122
  tokenData: {}
122
123
  });
124
+ },
125
+ async isPaidWelcomeEmailActive() {
126
+ if (!labs.isSet('welcomeEmails')) {
127
+ return false;
128
+ }
129
+ memberWelcomeEmailService.init();
130
+ return memberWelcomeEmailService.api.isMemberWelcomeEmailActive('paid');
123
131
  }
124
132
  });
125
133
 
@@ -186,6 +186,12 @@
186
186
  },
187
187
  "theme": {
188
188
  "maxAge": 31536000
189
+ },
190
+ "assets": {
191
+ "contentBasedHash": {
192
+ "enabled": false,
193
+ "maxSize": 100000
194
+ }
189
195
  }
190
196
  },
191
197
  "optimization": {
@@ -27,7 +27,9 @@ const GA_FEATURES = [
27
27
  'explore',
28
28
  'inboxlinks',
29
29
  'commentModeration',
30
- 'commentPermalinks'
30
+ 'commentPermalinks',
31
+ 'featurebaseFeedback',
32
+ 'welcomeEmails'
31
33
  ];
32
34
 
33
35
  // These features are considered publicly available and can be enabled/disabled by users
@@ -48,10 +50,8 @@ const PRIVATE_FEATURES = [
48
50
  'emailCustomization',
49
51
  'tagsX',
50
52
  'emailUniqueid',
51
- 'welcomeEmails',
52
53
  'themeTranslation',
53
54
  'indexnow',
54
- 'featurebaseFeedback',
55
55
  'transistor'
56
56
  ];
57
57
 
@@ -8,7 +8,8 @@ const urlUtils = new UrlUtils({
8
8
  getAdminUrl: config.getAdminUrl,
9
9
  assetBaseUrls: {
10
10
  media: config.get('urls:media'),
11
- files: config.get('urls:files')
11
+ files: config.get('urls:files'),
12
+ image: config.get('urls:image')
12
13
  },
13
14
  slugs: config.get('slugs').protected,
14
15
  redirectCacheMaxAge: config.get('caching:301:maxAge'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ghost",
3
- "version": "6.17.2",
3
+ "version": "6.18.1",
4
4
  "description": "The professional publishing platform",
5
5
  "author": "Ghost Foundation",
6
6
  "homepage": "https://ghost.org",
@@ -83,7 +83,7 @@
83
83
  "@tryghost/helpers": "1.1.97",
84
84
  "@tryghost/html-to-plaintext": "1.0.4",
85
85
  "@tryghost/http-cache-utils": "0.1.20",
86
- "@tryghost/i18n": "file:components/tryghost-i18n-6.17.2.tgz",
86
+ "@tryghost/i18n": "file:components/tryghost-i18n-6.18.1.tgz",
87
87
  "@tryghost/image-transform": "1.4.6",
88
88
  "@tryghost/job-manager": "1.0.3",
89
89
  "@tryghost/kg-card-factory": "5.1.7",
@@ -105,7 +105,7 @@
105
105
  "@tryghost/mw-vhost": "1.0.1",
106
106
  "@tryghost/nodemailer": "0.3.48",
107
107
  "@tryghost/nql": "0.12.8",
108
- "@tryghost/parse-email-address": "file:components/tryghost-parse-email-address-6.17.2.tgz",
108
+ "@tryghost/parse-email-address": "file:components/tryghost-parse-email-address-6.18.1.tgz",
109
109
  "@tryghost/pretty-cli": "1.2.47",
110
110
  "@tryghost/prometheus-metrics": "1.0.2",
111
111
  "@tryghost/promise": "0.3.15",
@@ -171,7 +171,7 @@
171
171
  "intl-messageformat": "5.4.3",
172
172
  "js-yaml": "4.1.0",
173
173
  "jsonc-parser": "3.3.1",
174
- "jsonpath": "1.1.1",
174
+ "jsonpath": "1.2.1",
175
175
  "jsonwebtoken": "8.5.1",
176
176
  "juice": "9.1.0",
177
177
  "keypair": "1.0.4",
@@ -271,8 +271,8 @@
271
271
  "jackspeak": "2.3.6",
272
272
  "moment": "2.24.0",
273
273
  "moment-timezone": "0.5.45",
274
- "@tryghost/i18n": "file:components/tryghost-i18n-6.17.2.tgz",
275
- "@tryghost/parse-email-address": "file:components/tryghost-parse-email-address-6.17.2.tgz"
274
+ "@tryghost/i18n": "file:components/tryghost-i18n-6.18.1.tgz",
275
+ "@tryghost/parse-email-address": "file:components/tryghost-parse-email-address-6.18.1.tgz"
276
276
  },
277
277
  "nx": {
278
278
  "targets": {