ghost 6.17.1 → 6.18.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 (217) hide show
  1. package/components/tryghost-i18n-6.18.0.tgz +0 -0
  2. package/components/{tryghost-parse-email-address-6.17.1.tgz → tryghost-parse-email-address-6.18.0.tgz} +0 -0
  3. package/core/built/admin/assets/{PolarAngleAxis-DVj7Sw2n.js → PolarAngleAxis-Dod0DwfL.js} +1 -1
  4. package/core/built/admin/assets/{_baseAssignValue-CrCkbXur.js → _baseAssignValue-DnkbkowM.js} +1 -1
  5. package/core/built/admin/assets/{a-large-small-BLquRSNr.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-C9yQgDwn.mjs} +2 -2
  36. package/core/built/admin/assets/admin-x-settings/{index-BXChUyr6.mjs → index-Cb0MgPGv.mjs} +30 -19
  37. package/core/built/admin/assets/admin-x-settings/{index-DYEF3NiM.mjs → index-DGoxlHIt.mjs} +2 -2
  38. package/core/built/admin/assets/admin-x-settings/{index-DdMo_pvD.mjs → index-jzFpi950.mjs} +2 -2
  39. package/core/built/admin/assets/admin-x-settings/{modals-Ck3AHDx3.mjs → modals-CCI0FGRh.mjs} +13 -12
  40. package/core/built/admin/assets/{at-sign-Cj4_ORaa.js → at-sign-Bz-SU-S_.js} +1 -1
  41. package/core/built/admin/assets/{audience-ACTBvUSt.js → audience-DmVdEmIe.js} +1 -1
  42. package/core/built/admin/assets/{avatar-flipboard-BeKL4BEF.js → avatar-flipboard-C7sxDVEM.js} +1 -1
  43. package/core/built/admin/assets/{bluesky-sharing-CP7ej5rz.js → bluesky-sharing-DJniSF6N.js} +1 -1
  44. package/core/built/admin/assets/{chart-B6jk72FT.js → chart-mYz3IJwm.js} +1 -1
  45. package/core/built/admin/assets/{chunk.524.6c12997c856012ea06f5.js → chunk.524.d2c4d1c78e72685d04c9.js} +6 -6
  46. package/core/built/admin/assets/{chunk.582.a0abd5b93eb1bc50335a.js → chunk.582.a3333324fa5a373a6148.js} +8 -8
  47. package/core/built/admin/assets/{code-editor-view-CVRtsOBM.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-YhtJexiS.js → copy-Bpq3uhnh.js} +1 -1
  50. package/core/built/admin/assets/{data-list-BPSXm3ej.js → data-list-BYNMbRIq.js} +1 -1
  51. package/core/built/admin/assets/{deleted-feed-item-D8BHDEHh.js → deleted-feed-item-B_AwUKZy.js} +1 -1
  52. package/core/built/admin/assets/{edit-profile-FrkkXMG7.js → edit-profile-DZo2ZcOu.js} +1 -1
  53. package/core/built/admin/assets/{empty-indicator-SsCyCd50.js → empty-indicator-Bn5wG9-T.js} +1 -1
  54. package/core/built/admin/assets/{en-ZWZBSdFY.js → en-D4zIrMLN.js} +1 -1
  55. package/core/built/admin/assets/{feed-BvVJCTy8.js → feed-Bgq4RarU.js} +1 -1
  56. package/core/built/admin/assets/{filters-BupJ4mUj.js → filters-CpVwu1Gk.js} +1 -1
  57. package/core/built/admin/assets/{gh-chart-Dp9VXeON.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-CRDHOGrZ.js → growth-CiPDnWxJ.js} +1 -1
  62. package/core/built/admin/assets/{hash-CPAX2PwN.js → hash-Bc1e38oH.js} +1 -1
  63. package/core/built/admin/assets/{inbox-DuT2edmx.js → inbox-BcZNBNhk.js} +1 -1
  64. package/core/built/admin/assets/{index-CQP2hNBo.js → index-BFEx_ouC.js} +1 -1
  65. package/core/built/admin/assets/{index-ByYZ5TiM.js → index-B_7QsC5T.js} +1 -1
  66. package/core/built/admin/assets/{index-BX3kQzlo.js → index-Bm5ZeLtt.js} +1 -1
  67. package/core/built/admin/assets/{index-Dk-Wh_Fw.js → index-BpcL7RmI.js} +2 -2
  68. package/core/built/admin/assets/{index--Nwo3958.js → index-Brg4tecQ.js} +1 -1
  69. package/core/built/admin/assets/{index-exFCQBCk.js → index-CMTzNTew.js} +29 -29
  70. package/core/built/admin/assets/{index-8ECl7z0l.js → index-CTfJflJ2.js} +1 -1
  71. package/core/built/admin/assets/{index-B2oDZJ9B.js → index-Cn32t_gv.js} +1 -1
  72. package/core/built/admin/assets/{index-BwUQiVrX.js → index-CsidX7Si.js} +1 -1
  73. package/core/built/admin/assets/{index-DnY4rls4.js → index-D212VvOz.js} +1 -1
  74. package/core/built/admin/assets/{index-bl5YmBtG.js → index-D67LJ_H4.js} +1 -1
  75. package/core/built/admin/assets/{index-BPYePamp.js → index-DU3bv9jz.js} +1 -1
  76. package/core/built/admin/assets/{index-CjzFwwKZ.js → index-Dv13wKfX.js} +1 -1
  77. package/core/built/admin/assets/{index-B1rQYn0h.js → index-ETsoQLbU.js} +1 -1
  78. package/core/built/admin/assets/{index-CeFAaI2v.js → index-ZYDRMgcT.js} +1 -1
  79. package/core/built/admin/assets/{koenig-lexical-eRS1y7PS.js → koenig-lexical-DZUWzN0P.js} +1 -1
  80. package/core/built/admin/assets/{kpi-card-DShFKtte.js → kpi-card-DENsK2xK.js} +1 -1
  81. package/core/built/admin/assets/{kpis-Bxyr_zYY.js → kpis-CDrs2iS1.js} +1 -1
  82. package/core/built/admin/assets/{label-hgdYQhmm.js → label-BznQtEEo.js} +1 -1
  83. package/core/built/admin/assets/{links-5NNDFN65.js → links-lpAC3T1p.js} +1 -1
  84. package/core/built/admin/assets/{lucide-react-f58FO8MB.js → lucide-react-CBigk-fq.js} +1 -1
  85. package/core/built/admin/assets/{main-layout-BDz_2UZs.js → main-layout-_SYQRjIl.js} +1 -1
  86. package/core/built/admin/assets/{message-square-text-6dTKbQKA.js → message-square-text-cV8O_qKq.js} +1 -1
  87. package/core/built/admin/assets/{minus-D2C19Fiu.js → minus-NvnQTlW7.js} +1 -1
  88. package/core/built/admin/assets/{modals-DzetQgXf.js → modals-XRSkribf.js} +3 -3
  89. package/core/built/admin/assets/{moderation-87QsKbyf.js → moderation-BQp1GEWG.js} +1 -1
  90. package/core/built/admin/assets/{newsletter-BhI9Lgkn.js → newsletter-foM6KNNV.js} +1 -1
  91. package/core/built/admin/assets/{newsletters-adLgF_Re.js → newsletters-BexdXUhn.js} +1 -1
  92. package/core/built/admin/assets/{note-BbPR57Yw.js → note-DuaUGOeZ.js} +1 -1
  93. package/core/built/admin/assets/{overview-CqyRlIVM.js → overview-DgKBNqyc.js} +1 -1
  94. package/core/built/admin/assets/{pagemenu-DlQF5jN4.js → pagemenu-CZyroidv.js} +1 -1
  95. package/core/built/admin/assets/{post-analytics-BudH7GCA.js → post-analytics-DLK2SOSQ.js} +1 -1
  96. package/core/built/admin/assets/{post-analytics-context-CxuzZluw.js → post-analytics-context-CF7C67-0.js} +1 -1
  97. package/core/built/admin/assets/{post-analytics-header-BV_8onIF.js → post-analytics-header-7GtJCx0W.js} +1 -1
  98. package/core/built/admin/assets/{post-share-modal-CnM21JFK.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-C5zAbpdG.js → posts-CL9UDYoW.js} +1 -1
  132. package/core/built/admin/assets/{repeat-BbSnXois.js → repeat-DgH39UKE.js} +1 -1
  133. package/core/built/admin/assets/{reply-CuCJJfZj.js → reply-DAaNxiy8.js} +1 -1
  134. package/core/built/admin/assets/{select-Bv-dkHeU.js → select-Cor2wFXT.js} +1 -1
  135. package/core/built/admin/assets/{settings-Cit2-dRQ.js → settings-BeumESEN.js} +5 -5
  136. package/core/built/admin/assets/{settings-wlK_oNsg.js → settings-xRx917Gj.js} +1 -1
  137. package/core/built/admin/assets/{sort-button-Dmyldyzm.js → sort-button-BNW3i4Lb.js} +1 -1
  138. package/core/built/admin/assets/{source-icon-DjyV6RS6.js → source-icon-DvDuzw73.js} +1 -1
  139. package/core/built/admin/assets/{sprout-jOQR37se.js → sprout-C3cc0c-K.js} +1 -1
  140. package/core/built/admin/assets/{square-IrYxuAJL.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-C4LbW8IV.js → stats-2Jelnn-Q.js} +1 -1
  154. package/core/built/admin/assets/{stats-view-BwaLDryw.js → stats-view-CESy8ELH.js} +1 -1
  155. package/core/built/admin/assets/{step-1-C0QApeYD.js → step-1-DrqdolAh.js} +1 -1
  156. package/core/built/admin/assets/{step-2-h0b6Njyb.js → step-2-DmEpKck5.js} +1 -1
  157. package/core/built/admin/assets/{step-3-CaM-Hz3F.js → step-3-Bus-0o0n.js} +1 -1
  158. package/core/built/admin/assets/{table-P4FGxGxN.js → table-BQUcKHfm.js} +1 -1
  159. package/core/built/admin/assets/{tabs-Cj65mCsJ.js → tabs-BmdL0X4U.js} +1 -1
  160. package/core/built/admin/assets/{tags-Dnv6zTag.js → tags-CLxXZlOO.js} +1 -1
  161. package/core/built/admin/assets/{tags-CDvEkvxr.js → tags-EchqlZUJ.js} +1 -1
  162. package/core/built/admin/assets/{tiers-Y3OCOT03.js → tiers-nCGyTly9.js} +1 -1
  163. package/core/built/admin/assets/{toggle-group-kZZo_vdJ.js → toggle-group-CM5uf7J1.js} +1 -1
  164. package/core/built/admin/assets/{topic-filter-Cn1_Z9I7.js → topic-filter-LTRvZ8aU.js} +1 -1
  165. package/core/built/admin/assets/{trash-ln90Rlgi.js → trash-u5BxolyH.js} +1 -1
  166. package/core/built/admin/assets/{url-helpers-B5_oMSPb.js → url-helpers-D41fEt51.js} +1 -1
  167. package/core/built/admin/assets/{use-growth-stats-DQsJauHH.js → use-growth-stats-BJ0O9ewi.js} +1 -1
  168. package/core/built/admin/assets/{use-infinite-virtual-scroll-4OIpSNMH.js → use-infinite-virtual-scroll-APZWciOk.js} +1 -1
  169. package/core/built/admin/assets/{use-simple-pagination-BedA-MMT.js → use-simple-pagination-DVRHeaAR.js} +1 -1
  170. package/core/built/admin/assets/{user-round-check-CYI147XP.js → user-round-check-B6j98D6d.js} +1 -1
  171. package/core/built/admin/assets/{wallet-cards-xpilOd6G.js → wallet-cards-KmOh29LP.js} +1 -1
  172. package/core/built/admin/assets/{web-VPMnLvnh.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/email-address/email-address-service-wrapper.js +3 -0
  189. package/core/server/services/email-address/email-address-service.js +11 -0
  190. package/core/server/services/email-address/email-address-service.ts +13 -0
  191. package/core/server/services/koenig/node-renderers/transistor-renderer.js +3 -8
  192. package/core/server/services/lib/member-signup-contexts.js +23 -0
  193. package/core/server/services/lib/member-signup-contexts.ts +22 -0
  194. package/core/server/services/member-welcome-emails/member-welcome-email-renderer.js +13 -7
  195. package/core/server/services/member-welcome-emails/service.js +8 -12
  196. package/core/server/services/members/api.js +3 -1
  197. package/core/server/services/members/members-api/controllers/router-controller.js +19 -2
  198. package/core/server/services/members/members-api/members-api.js +4 -2
  199. package/core/server/services/members/members-api/repositories/member-repository.js +1 -1
  200. package/core/server/services/members/members-api/services/payments-service.js +5 -0
  201. package/core/server/services/oembed/oembed-service.js +1 -1
  202. package/core/server/services/offers/application/offer-mapper.js +6 -7
  203. package/core/server/services/offers/application/offers-api.js +3 -3
  204. package/core/server/services/offers/domain/errors/index.js +2 -0
  205. package/core/server/services/offers/domain/models/offer.js +15 -3
  206. package/core/server/services/offers/offer-bookshelf-repository.js +4 -5
  207. package/core/server/services/stats/utils/tinybird.js +5 -3
  208. package/core/server/services/stripe/services/webhook/checkout-session-event-service.js +17 -1
  209. package/core/server/services/stripe/stripe-service.js +8 -0
  210. package/core/shared/config/defaults.json +6 -0
  211. package/core/shared/labs.js +3 -3
  212. package/core/shared/url-utils.js +2 -1
  213. package/package.json +6 -6
  214. package/tsconfig.tsbuildinfo +1 -1
  215. package/yarn.lock +23 -2
  216. package/components/tryghost-i18n-6.17.1.tgz +0 -0
  217. package/core/built/admin/assets/comments-CxjhZH_T.js +0 -1
@@ -24,6 +24,7 @@ export class EmailAddressService {
24
24
  #getDefaultEmail: () => EmailAddress;
25
25
  #getFallbackDomain: () => string | null;
26
26
  #getFallbackEmail: () => EmailAddress | null;
27
+ #getMembersSupportAddress: () => string;
27
28
  #isValidEmailAddress: (email: string) => boolean;
28
29
 
29
30
  constructor(dependencies: {
@@ -32,12 +33,14 @@ export class EmailAddressService {
32
33
  getFallbackDomain: () => string | null,
33
34
  getDefaultEmail: () => EmailAddress,
34
35
  getFallbackEmail: () => string | null,
36
+ getMembersSupportAddress: () => string,
35
37
  isValidEmailAddress: (email: string) => boolean
36
38
  }) {
37
39
  this.#getManagedEmailEnabled = dependencies.getManagedEmailEnabled;
38
40
  this.#getSendingDomain = dependencies.getSendingDomain;
39
41
  this.#getFallbackDomain = dependencies.getFallbackDomain;
40
42
  this.#getDefaultEmail = dependencies.getDefaultEmail;
43
+ this.#getMembersSupportAddress = dependencies.getMembersSupportAddress;
41
44
  this.#getFallbackEmail = () => {
42
45
  const fallbackAddress = dependencies.getFallbackEmail();
43
46
  if (!fallbackAddress) {
@@ -68,6 +71,16 @@ export class EmailAddressService {
68
71
  return this.#getFallbackEmail();
69
72
  }
70
73
 
74
+ /**
75
+ * Get the actual sender address for member emails after DMARC transformation.
76
+ * On managed platforms, this may differ from the configured support address to ensure DMARC compliance.
77
+ */
78
+ getMembersSupportAddress(): string {
79
+ const configuredAddress = this.#getMembersSupportAddress();
80
+ const transformed = this.getAddressFromString(configuredAddress);
81
+ return transformed.from.address;
82
+ }
83
+
71
84
  getAddressFromString(from: string, replyTo?: string): EmailAddresses {
72
85
  const parsedFrom = EmailAddressParser.parse(from);
73
86
  const parsedReplyTo = replyTo ? EmailAddressParser.parse(replyTo) : undefined;
@@ -16,14 +16,9 @@ function renderTransistorNode(node, options = {}) {
16
16
  function frontendTemplate(node, document, options) {
17
17
  const figure = document.createElement('figure');
18
18
  figure.setAttribute('class', 'kg-card kg-transistor-card');
19
- const memberUuid = options.memberUuid;
20
19
 
21
- if (!memberUuid) {
22
- // Transistor does not support public/non-member embeds for now, so we return null
23
- return null;
24
- }
25
-
26
- const embedUrl = new URL(`https://partner.transistor.fm/ghost/embed/${memberUuid}`);
20
+ // Use {uuid} placeholder - content.js will substitute with member UUID at request time
21
+ const embedUrl = new URL(`https://partner.transistor.fm/ghost/embed/{uuid}`);
27
22
 
28
23
  if (node.accentColor) {
29
24
  embedUrl.searchParams.set('color', node.accentColor.replace(/^#/, ''));
@@ -34,7 +29,7 @@ function frontendTemplate(node, document, options) {
34
29
 
35
30
  const iframe = document.createElement('iframe');
36
31
  iframe.setAttribute('width', '100%');
37
- iframe.setAttribute('height', '400');
32
+ iframe.setAttribute('height', '325');
38
33
  iframe.setAttribute('title', 'Transistor podcasts');
39
34
  iframe.setAttribute('frameborder', 'no');
40
35
  iframe.setAttribute('scrolling', 'no');
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SIGNUP_CONTEXTS = void 0;
4
+ exports.canWelcomeEmailReplaceSignupPaidEmail = canWelcomeEmailReplaceSignupPaidEmail;
5
+ // Signup context describes the sign-in state when the Stripe checkout session is created.
6
+ // NEEDS_MAGIC_LINK_EMAIL: No guaranteed sign-in path exists yet (custom/direct checkout paths).
7
+ // HAS_PRECHECKOUT_MAGIC_LINK: Ghost generated a signup magic-link before Stripe (standard Portal flow).
8
+ // ALREADY_AUTHENTICATED: Request came from a signed-in member identity (for example, opening a paid signup link directly).
9
+ exports.SIGNUP_CONTEXTS = {
10
+ NEEDS_MAGIC_LINK_EMAIL: 'needs_magic_link_email',
11
+ HAS_PRECHECKOUT_MAGIC_LINK: 'has_precheckout_magic_link',
12
+ ALREADY_AUTHENTICATED: 'already_authenticated'
13
+ };
14
+ /**
15
+ * Signup-paid email can be skipped when welcome email is active only if
16
+ * checkout already has a reliable sign-in path.
17
+ *
18
+ * - HAS_PRECHECKOUT_MAGIC_LINK: standard Portal flow generated signup link before Stripe
19
+ * - ALREADY_AUTHENTICATED: checkout request came from an already signed-in member
20
+ */
21
+ function canWelcomeEmailReplaceSignupPaidEmail(signupContext) {
22
+ return signupContext === exports.SIGNUP_CONTEXTS.HAS_PRECHECKOUT_MAGIC_LINK || signupContext === exports.SIGNUP_CONTEXTS.ALREADY_AUTHENTICATED;
23
+ }
@@ -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,7 +2,6 @@ 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');
7
6
  const mail = require('../mail');
8
7
  // @ts-expect-error type checker has trouble with the dynamic exporting in models
@@ -50,8 +49,14 @@ class MemberWelcomeEmailService {
50
49
  }
51
50
 
52
51
  async send({member, memberStatus}) {
52
+ if (!member.email) {
53
+ throw new errors.IncorrectUsageError({
54
+ message: MESSAGES.MISSING_RECIPIENT_EMAIL
55
+ });
56
+ }
57
+
53
58
  const name = member?.name ? `${member.name} at ` : '';
54
- logging.info(`${MEMBER_WELCOME_EMAIL_LOG_KEY} Sending welcome email to ${name}${member?.email}`);
59
+ logging.info(`${MEMBER_WELCOME_EMAIL_LOG_KEY} Sending welcome email to ${name}${member.email}`);
55
60
 
56
61
  const memberWelcomeEmail = this.#memberWelcomeEmails[memberStatus];
57
62
 
@@ -77,17 +82,8 @@ class MemberWelcomeEmailService {
77
82
  siteSettings: this.#getSiteSettings()
78
83
  });
79
84
 
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
- }
88
-
89
85
  await this.#mailer.send({
90
- to: toEmail,
86
+ to: member.email,
91
87
  subject,
92
88
  html,
93
89
  text,
@@ -19,6 +19,7 @@ const newslettersService = require('../newsletters');
19
19
  const memberAttributionService = require('../member-attribution');
20
20
  const emailSuppressionList = require('../email-suppression-list');
21
21
  const commentsService = require('../comments');
22
+ const emailAddressService = require('../email-address');
22
23
  const {t} = require('../i18n');
23
24
  const sentry = require('../../../shared/sentry');
24
25
 
@@ -254,7 +255,8 @@ function createApiInstance(config) {
254
255
  sentry,
255
256
  settingsHelpers,
256
257
  urlUtils,
257
- commentsService
258
+ commentsService,
259
+ emailAddressService: emailAddressService.service
258
260
  });
259
261
 
260
262
  return membersApiInstance;
@@ -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.',
@@ -75,6 +77,7 @@ module.exports = class RouterController {
75
77
  * @param {any} deps.settingsCache
76
78
  * @param {any} deps.settingsHelpers
77
79
  * @param {any} deps.urlUtils
80
+ * @param {any} deps.emailAddressService
78
81
  */
79
82
  constructor({
80
83
  offersAPI,
@@ -93,7 +96,8 @@ module.exports = class RouterController {
93
96
  sentry,
94
97
  settingsCache,
95
98
  settingsHelpers,
96
- urlUtils
99
+ urlUtils,
100
+ emailAddressService
97
101
  }) {
98
102
  this._offersAPI = offersAPI;
99
103
  this._paymentsService = paymentsService;
@@ -112,6 +116,7 @@ module.exports = class RouterController {
112
116
  this._settingsCache = settingsCache;
113
117
  this._settingsHelpers = settingsHelpers;
114
118
  this._urlUtils = urlUtils;
119
+ this._emailAddressService = emailAddressService;
115
120
  }
116
121
 
117
122
  async ensureStripe(_req, res, next) {
@@ -389,6 +394,12 @@ module.exports = class RouterController {
389
394
  });
390
395
  }
391
396
 
397
+ if (!offer.tier) {
398
+ throw new BadRequestError({
399
+ message: 'Offer does not have a tier'
400
+ });
401
+ }
402
+
392
403
  tier = await this._tiersService.api.read(offer.tier.id);
393
404
  cadence = offer.cadence;
394
405
  } else if (tierId) {
@@ -466,6 +477,8 @@ module.exports = class RouterController {
466
477
  }
467
478
 
468
479
  const member = options.member;
480
+ /** @type {SignupContext} */
481
+ let ghostSignupContext = (options.isAuthenticated && member) ? SIGNUP_CONTEXTS.ALREADY_AUTHENTICATED : SIGNUP_CONTEXTS.NEEDS_MAGIC_LINK_EMAIL;
469
482
 
470
483
  if (!member && options.email) {
471
484
  // Create a signup link if there is no member with this email address
@@ -482,6 +495,7 @@ module.exports = class RouterController {
482
495
  // Redirect to the original success url after sign up
483
496
  referrer: options.successUrl
484
497
  });
498
+ ghostSignupContext = SIGNUP_CONTEXTS.HAS_PRECHECKOUT_MAGIC_LINK;
485
499
  }
486
500
 
487
501
  if (member) {
@@ -506,6 +520,9 @@ module.exports = class RouterController {
506
520
  }
507
521
  }
508
522
 
523
+ // Set by server to distinguish between checkout flows in Stripe webhooks.
524
+ options.metadata.ghostSignupContext = ghostSignupContext;
525
+
509
526
  try {
510
527
  const paymentLink = await this._paymentsService.getPaymentLink(options);
511
528
 
@@ -733,7 +750,7 @@ module.exports = class RouterController {
733
750
 
734
751
  const inboxLinks = await getInboxLinks({
735
752
  recipient: normalizedEmail,
736
- sender: this._settingsHelpers.getMembersSupportAddress(),
753
+ sender: this._emailAddressService.getMembersSupportAddress(),
737
754
  dnsResolver: this.#inboxLinksDnsResolver
738
755
  });
739
756
  if (inboxLinks) {
@@ -79,7 +79,8 @@ module.exports = function MembersAPI({
79
79
  sentry,
80
80
  settingsHelpers,
81
81
  urlUtils,
82
- commentsService
82
+ commentsService,
83
+ emailAddressService
83
84
  }) {
84
85
  const tokenService = new TokenService({
85
86
  privateKey,
@@ -213,7 +214,8 @@ module.exports = function MembersAPI({
213
214
  settingsCache,
214
215
  settingsHelpers,
215
216
  sentry,
216
- urlUtils
217
+ urlUtils,
218
+ emailAddressService
217
219
  });
218
220
 
219
221
  const wellKnownController = new WellKnownController({
@@ -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'),