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
@@ -0,0 +1,186 @@
1
+ #!/bin/bash
2
+
3
+ # benchmark-top-pages.sh - Compare performance of api_top_pages vs api_top_pages_v3
4
+ #
5
+ # Usage: ./benchmark-top-pages.sh [iterations]
6
+ #
7
+ # Prerequisites: yarn dev:analytics
8
+
9
+ set -e
10
+
11
+ # Configuration
12
+ TB_HOST="${TB_HOST:-http://localhost:7181}"
13
+ ITERATIONS="${1:-5}"
14
+
15
+ # Get site_uuid and token from Docker
16
+ echo "============================================"
17
+ echo "Tinybird Top Pages Benchmark"
18
+ echo "============================================"
19
+
20
+ # Get token
21
+ TB_TOKEN=$(docker run --rm -v ghost-dev_shared-config:/config alpine cat /config/.env.tinybird 2>/dev/null | grep TINYBIRD_ADMIN_TOKEN | cut -d= -f2)
22
+ if [ -z "$TB_TOKEN" ]; then
23
+ echo "Error: Could not find Tinybird token. Is yarn dev:analytics running?"
24
+ exit 1
25
+ fi
26
+
27
+ # Get site_uuid - first try from analytics data, fallback to Ghost database
28
+ SITE_UUID=$(curl -s -H "Authorization: Bearer $TB_TOKEN" \
29
+ "${TB_HOST}/v0/sql?q=SELECT%20site_uuid%20FROM%20_mv_hits%20WHERE%20site_uuid%20!=%20'mock_site_uuid'%20LIMIT%201" | tr -d '\n')
30
+ if [ -z "$SITE_UUID" ] || [ "$SITE_UUID" = "" ]; then
31
+ # Fallback to Ghost database
32
+ SITE_UUID=$(docker exec ghost-dev-mysql mysql -uroot -proot ghost_dev -N -e "SELECT value FROM settings WHERE \`key\`='db_hash'" 2>/dev/null | tr -d '\r\n')
33
+ fi
34
+ if [ -z "$SITE_UUID" ]; then
35
+ echo "Error: Could not get site_uuid"
36
+ exit 1
37
+ fi
38
+
39
+ # Calculate date range (last 30 days)
40
+ DATE_TO=$(date +%Y-%m-%d)
41
+ DATE_FROM=$(date -v-30d +%Y-%m-%d 2>/dev/null || date -d '30 days ago' +%Y-%m-%d)
42
+ TIMEZONE="Etc/UTC"
43
+
44
+ echo "Site UUID: $SITE_UUID"
45
+ echo "Date Range: $DATE_FROM to $DATE_TO"
46
+ echo "Iterations: $ITERATIONS"
47
+ echo "============================================"
48
+ echo ""
49
+
50
+ # Check data volume
51
+ echo "Checking data volume..."
52
+ DATA_COUNT=$(curl -s -H "Authorization: Bearer $TB_TOKEN" \
53
+ "${TB_HOST}/v0/pipes/api_top_pages.json?site_uuid=${SITE_UUID}&date_from=${DATE_FROM}&date_to=${DATE_TO}&timezone=${TIMEZONE}&limit=1" \
54
+ | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('statistics',{}).get('rows_read', 'unknown'))" 2>/dev/null || echo "unknown")
55
+ echo "Rows in date range: ~${DATA_COUNT}"
56
+ echo ""
57
+
58
+ # Benchmark function
59
+ benchmark() {
60
+ local version=$1
61
+ local label=$2
62
+ local endpoint="api_top_pages"
63
+
64
+ if [ -n "$version" ]; then
65
+ endpoint="${endpoint}_${version}"
66
+ fi
67
+
68
+ local url="${TB_HOST}/v0/pipes/${endpoint}.json?site_uuid=${SITE_UUID}&date_from=${DATE_FROM}&date_to=${DATE_TO}&timezone=${TIMEZONE}"
69
+
70
+ echo "Testing: $label"
71
+ echo "Endpoint: $endpoint"
72
+
73
+ local times=()
74
+ local total_time=0
75
+
76
+ for i in $(seq 1 $ITERATIONS); do
77
+ # Use curl timing
78
+ local result=$(curl -s -w "%{time_total}" -o /tmp/tb_response.json \
79
+ -H "Authorization: Bearer $TB_TOKEN" \
80
+ "$url")
81
+
82
+ local time_ms=$(python3 -c "print(int(float('$result') * 1000))")
83
+ times+=($time_ms)
84
+ total_time=$((total_time + time_ms))
85
+
86
+ # Check for errors
87
+ local error=$(python3 -c "import json; d=json.load(open('/tmp/tb_response.json')); print(d.get('error',''))" 2>/dev/null)
88
+ if [ -n "$error" ]; then
89
+ echo " Iteration $i: ERROR - $error"
90
+ else
91
+ local stats=$(python3 -c "
92
+ import json
93
+ d=json.load(open('/tmp/tb_response.json'))
94
+ rows=len(d.get('data',[]))
95
+ stats=d.get('statistics',{})
96
+ rows_read=stats.get('rows_read',0)
97
+ print(f'{rows}|{rows_read}')" 2>/dev/null)
98
+ local rows=$(echo "$stats" | cut -d'|' -f1)
99
+ local rows_read=$(echo "$stats" | cut -d'|' -f2)
100
+ printf " Iteration %d: %4dms | %d results | %s rows scanned\n" "$i" "$time_ms" "$rows" "$rows_read"
101
+ fi
102
+ done
103
+
104
+ if [ ${#times[@]} -gt 0 ]; then
105
+ local avg=$((total_time / ${#times[@]}))
106
+
107
+ # Calculate min/max
108
+ local min=${times[0]}
109
+ local max=${times[0]}
110
+ for t in "${times[@]}"; do
111
+ [ $t -lt $min ] && min=$t
112
+ [ $t -gt $max ] && max=$t
113
+ done
114
+
115
+ # Get final stats for summary
116
+ local final_rows_read=$(python3 -c "import json; print(json.load(open('/tmp/tb_response.json')).get('statistics',{}).get('rows_read',0))" 2>/dev/null)
117
+
118
+ echo " ----------------------------------------"
119
+ printf " Avg: %dms | Min: %dms | Max: %dms | Rows: %s\n" "$avg" "$min" "$max" "$final_rows_read"
120
+ fi
121
+ echo ""
122
+ }
123
+
124
+ # Store results for comparison
125
+ V1_STATS=""
126
+ V3_STATS=""
127
+
128
+ run_benchmark() {
129
+ local version=$1
130
+ local label=$2
131
+ benchmark "$version" "$label"
132
+
133
+ # Capture stats from last run
134
+ local stats=$(python3 -c "
135
+ import json
136
+ d=json.load(open('/tmp/tb_response.json'))
137
+ s=d.get('statistics',{})
138
+ print(f\"{s.get('elapsed',0)*1000:.1f}|{s.get('rows_read',0)}\")" 2>/dev/null)
139
+
140
+ if [ "$version" = "" ]; then
141
+ V1_STATS="$stats"
142
+ else
143
+ V3_STATS="$stats"
144
+ fi
145
+ }
146
+
147
+ # Run benchmarks
148
+ run_benchmark "" "api_top_pages (unversioned)"
149
+ run_benchmark "v3" "api_top_pages_v3 (daily MV)"
150
+
151
+ # Summary comparison
152
+ echo "============================================"
153
+ echo "Performance Comparison Summary"
154
+ echo "============================================"
155
+
156
+ V1_TIME=$(echo "$V1_STATS" | cut -d'|' -f1)
157
+ V1_ROWS=$(echo "$V1_STATS" | cut -d'|' -f2)
158
+ V3_TIME=$(echo "$V3_STATS" | cut -d'|' -f1)
159
+ V3_ROWS=$(echo "$V3_STATS" | cut -d'|' -f2)
160
+
161
+ python3 << PYTHON_SUMMARY
162
+ v1_time = float("$V1_TIME")
163
+ v1_rows = int("$V1_ROWS")
164
+ v3_time = float("$V3_TIME")
165
+ v3_rows = int("$V3_ROWS")
166
+
167
+ print(f"api_top_pages: {v1_time:>8.1f}ms | {v1_rows:>12,} rows scanned")
168
+ print(f"api_top_pages_v3: {v3_time:>8.1f}ms | {v3_rows:>12,} rows scanned")
169
+ print()
170
+
171
+ if v1_time > 0 and v3_time > 0:
172
+ time_ratio = v1_time / v3_time
173
+ if time_ratio >= 1:
174
+ print(f"Speed: v3 is {time_ratio:.1f}x faster")
175
+ else:
176
+ print(f"Speed: v1 is {1/time_ratio:.1f}x faster")
177
+
178
+ if v1_rows > 0 and v3_rows > 0:
179
+ rows_ratio = v1_rows / v3_rows
180
+ print(f"Efficiency: v3 scans {rows_ratio:.1f}x fewer rows")
181
+ PYTHON_SUMMARY
182
+
183
+ echo "============================================"
184
+
185
+ # Cleanup
186
+ rm -f /tmp/tb_response.json
@@ -0,0 +1,262 @@
1
+ #!/bin/bash
2
+
3
+ # compare-top-pages.sh - Validate api_top_pages vs api_top_pages_v3 results
4
+ #
5
+ # Usage: ./compare-top-pages.sh [-l limit] [-d days] [-t tolerance] [-v]
6
+ #
7
+ # Prerequisites: yarn dev:analytics
8
+ #
9
+ # Expected: v3 counts may be 1-2 lower for pages where sessions cross midnight
10
+ # at date boundaries. This is correct behavior - v1 over-counts by including
11
+ # page views outside the date range when sessions span multiple days.
12
+
13
+ set -e
14
+
15
+ # Default configuration
16
+ TB_HOST="${TB_HOST:-http://localhost:7181}"
17
+ LIMIT=50
18
+ DAYS=30
19
+ TOLERANCE=5
20
+ VERBOSE=false
21
+
22
+ # Parse arguments
23
+ while [[ $# -gt 0 ]]; do
24
+ case $1 in
25
+ -l|--limit)
26
+ LIMIT="$2"
27
+ shift 2
28
+ ;;
29
+ -d|--days)
30
+ DAYS="$2"
31
+ shift 2
32
+ ;;
33
+ -t|--tolerance)
34
+ TOLERANCE="$2"
35
+ shift 2
36
+ ;;
37
+ -v|--verbose)
38
+ VERBOSE=true
39
+ shift
40
+ ;;
41
+ -h|--help)
42
+ head -20 "$0" | tail -17
43
+ exit 0
44
+ ;;
45
+ *)
46
+ echo "Unknown option: $1"
47
+ exit 1
48
+ ;;
49
+ esac
50
+ done
51
+
52
+ echo "============================================"
53
+ echo "Tinybird Top Pages Comparison"
54
+ echo "============================================"
55
+
56
+ # Get token
57
+ TB_TOKEN=$(docker run --rm -v ghost-dev_shared-config:/config alpine cat /config/.env.tinybird 2>/dev/null | grep TINYBIRD_ADMIN_TOKEN | cut -d= -f2)
58
+ if [ -z "$TB_TOKEN" ]; then
59
+ echo "Error: Could not find Tinybird token. Is yarn dev:analytics running?"
60
+ exit 1
61
+ fi
62
+
63
+ # Get site_uuid from analytics data (check what's actually in the data)
64
+ SITE_UUID=$(curl -s -H "Authorization: Bearer $TB_TOKEN" \
65
+ "${TB_HOST}/v0/sql?q=SELECT%20site_uuid%20FROM%20_mv_hits%20WHERE%20site_uuid%20!=%20'mock_site_uuid'%20LIMIT%201" | tr -d '\n')
66
+
67
+ if [ -z "$SITE_UUID" ] || [ "$SITE_UUID" = "" ]; then
68
+ echo "Error: Could not get site_uuid from analytics data"
69
+ exit 1
70
+ fi
71
+
72
+ # Calculate date range
73
+ DATE_TO=$(date +%Y-%m-%d)
74
+ DATE_FROM=$(date -v-${DAYS}d +%Y-%m-%d 2>/dev/null || date -d "${DAYS} days ago" +%Y-%m-%d)
75
+ TIMEZONE="Etc/UTC"
76
+
77
+ echo "Site UUID: $SITE_UUID"
78
+ echo "Date Range: $DATE_FROM to $DATE_TO"
79
+ echo "Limit: $LIMIT results"
80
+ echo "Tolerance: ${TOLERANCE}%"
81
+ echo "============================================"
82
+ echo ""
83
+
84
+ # Create temp files
85
+ V1_RESPONSE=$(mktemp)
86
+ V3_RESPONSE=$(mktemp)
87
+
88
+ cleanup() {
89
+ rm -f "$V1_RESPONSE" "$V3_RESPONSE"
90
+ }
91
+ trap cleanup EXIT
92
+
93
+ # Fetch from both endpoints
94
+ echo "Fetching from api_top_pages..."
95
+ curl -s -H "Authorization: Bearer $TB_TOKEN" \
96
+ "${TB_HOST}/v0/pipes/api_top_pages.json?site_uuid=${SITE_UUID}&date_from=${DATE_FROM}&date_to=${DATE_TO}&timezone=${TIMEZONE}&limit=${LIMIT}" \
97
+ > "$V1_RESPONSE"
98
+
99
+ echo "Fetching from api_top_pages_v3..."
100
+ curl -s -H "Authorization: Bearer $TB_TOKEN" \
101
+ "${TB_HOST}/v0/pipes/api_top_pages_v3.json?site_uuid=${SITE_UUID}&date_from=${DATE_FROM}&date_to=${DATE_TO}&timezone=${TIMEZONE}&limit=${LIMIT}" \
102
+ > "$V3_RESPONSE"
103
+
104
+ # Check for errors
105
+ V1_ERROR=$(python3 -c "import json; d=json.load(open('$V1_RESPONSE')); print(d.get('error',''))" 2>/dev/null)
106
+ V3_ERROR=$(python3 -c "import json; d=json.load(open('$V3_RESPONSE')); print(d.get('error',''))" 2>/dev/null)
107
+
108
+ if [ -n "$V1_ERROR" ]; then
109
+ echo "Error from api_top_pages: $V1_ERROR"
110
+ exit 1
111
+ fi
112
+
113
+ if [ -n "$V3_ERROR" ]; then
114
+ echo "Error from api_top_pages_v3: $V3_ERROR"
115
+ exit 1
116
+ fi
117
+
118
+ echo ""
119
+
120
+ # Compare results using Python
121
+ export V1_RESPONSE V3_RESPONSE TOLERANCE VERBOSE
122
+ python3 << 'PYTHON_SCRIPT'
123
+ import json
124
+ import sys
125
+ import os
126
+
127
+ v1_file = os.environ['V1_RESPONSE']
128
+ v3_file = os.environ['V3_RESPONSE']
129
+ tolerance = float(os.environ['TOLERANCE'])
130
+ verbose = os.environ['VERBOSE'] == 'true'
131
+
132
+ with open(v1_file) as f:
133
+ v1_data = json.load(f)
134
+
135
+ with open(v3_file) as f:
136
+ v3_data = json.load(f)
137
+
138
+ v1_results = {(r['pathname'], r['post_uuid']): r['visits'] for r in v1_data['data']}
139
+ v3_results = {(r['pathname'], r['post_uuid']): r['visits'] for r in v3_data['data']}
140
+
141
+ # Statistics
142
+ v1_stats = v1_data.get('statistics', {})
143
+ v3_stats = v3_data.get('statistics', {})
144
+
145
+ print("=== Performance Statistics ===")
146
+ print(f"api_top_pages: {v1_stats.get('elapsed', 0)*1000:.2f}ms | {v1_stats.get('rows_read', 0):,} rows read")
147
+ print(f"api_top_pages_v3: {v3_stats.get('elapsed', 0)*1000:.2f}ms | {v3_stats.get('rows_read', 0):,} rows read")
148
+ print()
149
+
150
+ # Combine all keys
151
+ all_keys = set(v1_results.keys()) | set(v3_results.keys())
152
+
153
+ differences = []
154
+ matches = []
155
+ only_in_v1 = []
156
+ only_in_v3 = []
157
+
158
+ for key in all_keys:
159
+ pathname, post_uuid = key
160
+ v1_val = v1_results.get(key)
161
+ v3_val = v3_results.get(key)
162
+
163
+ if v1_val is None:
164
+ only_in_v3.append((pathname, post_uuid, v3_val))
165
+ elif v3_val is None:
166
+ only_in_v1.append((pathname, post_uuid, v1_val))
167
+ else:
168
+ diff = abs(v1_val - v3_val)
169
+ diff_pct = (diff / max(v1_val, 1)) * 100
170
+
171
+ if diff > 0:
172
+ differences.append({
173
+ 'pathname': pathname,
174
+ 'post_uuid': post_uuid,
175
+ 'v1': v1_val,
176
+ 'v3': v3_val,
177
+ 'diff': diff,
178
+ 'diff_pct': diff_pct
179
+ })
180
+ else:
181
+ matches.append({
182
+ 'pathname': pathname,
183
+ 'post_uuid': post_uuid,
184
+ 'visits': v1_val
185
+ })
186
+
187
+ # Sort differences by absolute difference
188
+ differences.sort(key=lambda x: x['diff'], reverse=True)
189
+
190
+ print("=== Comparison Results ===")
191
+ print(f"Total pages in v1: {len(v1_results)}")
192
+ print(f"Total pages in v3: {len(v3_results)}")
193
+ print(f"Exact matches: {len(matches)}")
194
+ print(f"Differences: {len(differences)}")
195
+ print(f"Only in v1: {len(only_in_v1)}")
196
+ print(f"Only in v3: {len(only_in_v3)}")
197
+ print()
198
+
199
+ # Check for significant differences
200
+ significant = [d for d in differences if d['diff_pct'] > tolerance]
201
+
202
+ if differences:
203
+ print("=== Differences Found ===")
204
+ print(f"{'Pathname':<60} {'V1':>8} {'V3':>8} {'Diff':>8} {'Diff%':>8}")
205
+ print("-" * 96)
206
+
207
+ for d in differences[:20]: # Show top 20 differences
208
+ pathname = d['pathname'][:58] if len(d['pathname']) > 58 else d['pathname']
209
+ marker = " *" if d['diff_pct'] > tolerance else ""
210
+ print(f"{pathname:<60} {d['v1']:>8} {d['v3']:>8} {d['diff']:>+8} {d['diff_pct']:>7.2f}%{marker}")
211
+
212
+ if len(differences) > 20:
213
+ print(f"... and {len(differences) - 20} more differences")
214
+ print()
215
+ print("(* = exceeds tolerance threshold)")
216
+
217
+ if only_in_v1:
218
+ print()
219
+ print("=== Pages only in api_top_pages (v1) ===")
220
+ for pathname, post_uuid, visits in only_in_v1[:10]:
221
+ print(f" {pathname:<60} visits: {visits}")
222
+ if len(only_in_v1) > 10:
223
+ print(f" ... and {len(only_in_v1) - 10} more")
224
+
225
+ if only_in_v3:
226
+ print()
227
+ print("=== Pages only in api_top_pages_v3 ===")
228
+ for pathname, post_uuid, visits in only_in_v3[:10]:
229
+ print(f" {pathname:<60} visits: {visits}")
230
+ if len(only_in_v3) > 10:
231
+ print(f" ... and {len(only_in_v3) - 10} more")
232
+
233
+ if verbose and matches:
234
+ print()
235
+ print("=== Exact Matches ===")
236
+ for m in matches[:20]:
237
+ print(f" {m['pathname']:<60} visits: {m['visits']}")
238
+ if len(matches) > 20:
239
+ print(f" ... and {len(matches) - 20} more matches")
240
+
241
+ print()
242
+ print("============================================")
243
+
244
+ # Check for unexpected differences (v3 should always be <= v1)
245
+ unexpected = [d for d in differences if d['v3'] > d['v1']]
246
+
247
+ # Exit status based on differences
248
+ if unexpected:
249
+ print(f"FAIL: {len(unexpected)} pages have v3 > v1 (unexpected - v3 should never exceed v1)")
250
+ sys.exit(1)
251
+ elif significant:
252
+ print(f"WARN: {len(significant)} pages exceed {tolerance}% tolerance (but v3 <= v1 as expected)")
253
+ print(" Small differences are expected due to cross-midnight session handling.")
254
+ sys.exit(0)
255
+ elif differences:
256
+ print(f"OK: {len(differences)} pages have minor differences (within {tolerance}% tolerance)")
257
+ print(" v3 counts are lower as expected (more accurate date filtering).")
258
+ sys.exit(0)
259
+ else:
260
+ print("PASS: All results match exactly!")
261
+ sys.exit(0)
262
+ PYTHON_SCRIPT
@@ -0,0 +1,112 @@
1
+
2
+ - name: Date range
3
+ description: All fixture data - uses pre-aggregated daily MV
4
+ parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC
5
+ expected_result: |
6
+ {"post_uuid":"6b8635fb-292f-4422-9fe4-d76cfab2ba31","pathname":"\/blog\/hello-world\/","visits":9}
7
+ {"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","pathname":"\/about\/","visits":8}
8
+ {"post_uuid":"","pathname":"\/","visits":7}
9
+ {"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1dd","pathname":"\/blog\/hello-world\/","visits":1}
10
+
11
+ - name: Filtered by pathname - /about/
12
+ description: Filtered by pathname - /about/
13
+ parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&pathname=%2Fabout%2F
14
+ expected_result: |
15
+ {"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","pathname":"\/about\/","visits":8}
16
+
17
+ - name: Filtered by post_uuid - 06b1b0c9-fb53-4a15-a060-3db3fde7b1fc (/about/)
18
+ description: Filtered by post_uuid - 06b1b0c9-fb53-4a15-a060-3db3fde7b1fc (/about/)
19
+ parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&post_uuid=06b1b0c9-fb53-4a15-a060-3db3fde7b1fc
20
+ expected_result: |
21
+ {"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","pathname":"\/about\/","visits":8}
22
+
23
+ - name: Filtered by member status - paid
24
+ description: Filtered by member status - paid (includes comped)
25
+ parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&member_status=paid
26
+ expected_result: |
27
+ {"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","pathname":"\/about\/","visits":3}
28
+ {"post_uuid":"6b8635fb-292f-4422-9fe4-d76cfab2ba31","pathname":"\/blog\/hello-world\/","visits":3}
29
+ {"post_uuid":"","pathname":"\/","visits":2}
30
+
31
+ - name: Filtered by member status - free
32
+ description: Filtered by member status - free
33
+ parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&member_status=free
34
+ expected_result: |
35
+ {"post_uuid":"","pathname":"\/","visits":4}
36
+ {"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","pathname":"\/about\/","visits":3}
37
+ {"post_uuid":"6b8635fb-292f-4422-9fe4-d76cfab2ba31","pathname":"\/blog\/hello-world\/","visits":2}
38
+
39
+ - name: Filtered by member status - undefined
40
+ description: Filtered by member status - undefined
41
+ parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&member_status=undefined
42
+ expected_result: |
43
+ {"post_uuid":"6b8635fb-292f-4422-9fe4-d76cfab2ba31","pathname":"\/blog\/hello-world\/","visits":4}
44
+ {"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","pathname":"\/about\/","visits":2}
45
+ {"post_uuid":"","pathname":"\/","visits":1}
46
+ {"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1dd","pathname":"\/blog\/hello-world\/","visits":1}
47
+
48
+ - name: Filtered by timezone - America/Los_Angeles
49
+ description: Filtered by timezone - America/Los_Angeles (8 hours behind UTC)
50
+ parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=America/Los_Angeles
51
+ expected_result: |
52
+ {"post_uuid":"6b8635fb-292f-4422-9fe4-d76cfab2ba31","pathname":"\/blog\/hello-world\/","visits":9}
53
+ {"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","pathname":"\/about\/","visits":8}
54
+ {"post_uuid":"","pathname":"\/","visits":7}
55
+ {"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1dd","pathname":"\/blog\/hello-world\/","visits":1}
56
+
57
+ - name: Test with post_type - post
58
+ description: Test with post_type - post
59
+ parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&post_type=post
60
+ expected_result: |
61
+ {"post_uuid":"6b8635fb-292f-4422-9fe4-d76cfab2ba31","pathname":"\/blog\/hello-world\/","visits":9}
62
+ {"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1dd","pathname":"\/blog\/hello-world\/","visits":1}
63
+
64
+ - name: Test with post_type - page
65
+ description: Test with post_type - page (includes empty post_type)
66
+ parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&post_type=page
67
+ expected_result: |
68
+ {"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","pathname":"\/about\/","visits":8}
69
+ {"post_uuid":"","pathname":"\/","visits":7}
70
+
71
+ - name: Test with multiple filters combined - member_status and pathname
72
+ description: Test combining member_status and pathname filters
73
+ parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&member_status=paid&pathname=%2Fabout%2F
74
+ expected_result: |
75
+ {"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","pathname":"\/about\/","visits":3}
76
+
77
+ - name: Test with multiple filters combined - post_type and member_status
78
+ description: Test combining post_type and member_status filters
79
+ parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&post_type=post&member_status=undefined
80
+ expected_result: |
81
+ {"post_uuid":"6b8635fb-292f-4422-9fe4-d76cfab2ba31","pathname":"\/blog\/hello-world\/","visits":4}
82
+ {"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1dd","pathname":"\/blog\/hello-world\/","visits":1}
83
+
84
+ - name: Test pagination - skip
85
+ description: Test pagination with skip parameter
86
+ parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&skip=1&limit=2
87
+ expected_result: |
88
+ {"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","pathname":"\/about\/","visits":8}
89
+ {"post_uuid":"","pathname":"\/","visits":7}
90
+
91
+ - name: Test pagination - limit
92
+ description: Test pagination with limit parameter
93
+ parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&limit=2
94
+ expected_result: |
95
+ {"post_uuid":"6b8635fb-292f-4422-9fe4-d76cfab2ba31","pathname":"\/blog\/hello-world\/","visits":9}
96
+ {"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","pathname":"\/about\/","visits":8}
97
+
98
+ - name: Single day query
99
+ description: Test querying a single day
100
+ parameters: site_uuid=mock_site_uuid&date_from=2100-01-04&date_to=2100-01-04&timezone=Etc/UTC
101
+ expected_result: |
102
+ {"post_uuid":"6b8635fb-292f-4422-9fe4-d76cfab2ba31","pathname":"\/blog\/hello-world\/","visits":3}
103
+ {"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","pathname":"\/about\/","visits":2}
104
+ {"post_uuid":"","pathname":"\/","visits":1}
105
+
106
+ - name: Partial date range
107
+ description: Test querying a partial date range (first 3 days)
108
+ parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-03&timezone=Etc/UTC
109
+ expected_result: |
110
+ {"post_uuid":"","pathname":"\/","visits":5}
111
+ {"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","pathname":"\/about\/","visits":3}
112
+ {"post_uuid":"6b8635fb-292f-4422-9fe4-d76cfab2ba31","pathname":"\/blog\/hello-world\/","visits":3}
@@ -1,6 +1,10 @@
1
1
  const ghostBookshelf = require('./base');
2
+ const logging = require('@tryghost/logging');
2
3
  const urlUtils = require('../../shared/url-utils');
3
4
  const lexicalLib = require('../lib/lexical');
5
+ const {MEMBER_WELCOME_EMAIL_SLUGS} = require('../services/member-welcome-emails/constants');
6
+
7
+ const MEMBER_WELCOME_EMAIL_SLUG_SET = new Set(Object.values(MEMBER_WELCOME_EMAIL_SLUGS));
4
8
 
5
9
  const AutomatedEmail = ghostBookshelf.Model.extend({
6
10
  tableName: 'automated_emails',
@@ -33,6 +37,36 @@ const AutomatedEmail = ghostBookshelf.Model.extend({
33
37
  }
34
38
 
35
39
  return attrs;
40
+ },
41
+
42
+ onSaved(model) {
43
+ if (!model?.id) {
44
+ return;
45
+ }
46
+
47
+ const slug = model.get('slug');
48
+
49
+ if (!MEMBER_WELCOME_EMAIL_SLUG_SET.has(slug)) {
50
+ return;
51
+ }
52
+
53
+ const previousStatus = model.previous('status');
54
+ const currentStatus = model.get('status');
55
+ const isNewModel = previousStatus === undefined;
56
+ const isEnableTransition = currentStatus === 'active' && (isNewModel || previousStatus === 'inactive');
57
+ const isDisableTransition = previousStatus === 'active' && currentStatus === 'inactive';
58
+
59
+ if (!isEnableTransition && !isDisableTransition) {
60
+ return;
61
+ }
62
+
63
+ logging.info({
64
+ system: {
65
+ event: isEnableTransition ? 'welcome_email.enabled' : 'welcome_email.disabled',
66
+ automated_email_id: model.id,
67
+ slug
68
+ }
69
+ }, isEnableTransition ? 'Welcome email enabled' : 'Welcome email disabled');
36
70
  }
37
71
  });
38
72
 
@@ -33,6 +33,9 @@ class EmailAddressServiceWrapper {
33
33
  getFallbackEmail: () => {
34
34
  return config.get('hostSettings:managedEmail:fallbackAddress') || null;
35
35
  },
36
+ getMembersSupportAddress: () => {
37
+ return settingsHelpers.getMembersSupportAddress();
38
+ },
36
39
  isValidEmailAddress: (emailAddress) => {
37
40
  return validator.isEmail(emailAddress);
38
41
  }
@@ -12,12 +12,14 @@ class EmailAddressService {
12
12
  #getDefaultEmail;
13
13
  #getFallbackDomain;
14
14
  #getFallbackEmail;
15
+ #getMembersSupportAddress;
15
16
  #isValidEmailAddress;
16
17
  constructor(dependencies) {
17
18
  this.#getManagedEmailEnabled = dependencies.getManagedEmailEnabled;
18
19
  this.#getSendingDomain = dependencies.getSendingDomain;
19
20
  this.#getFallbackDomain = dependencies.getFallbackDomain;
20
21
  this.#getDefaultEmail = dependencies.getDefaultEmail;
22
+ this.#getMembersSupportAddress = dependencies.getMembersSupportAddress;
21
23
  this.#getFallbackEmail = () => {
22
24
  const fallbackAddress = dependencies.getFallbackEmail();
23
25
  if (!fallbackAddress) {
@@ -42,6 +44,15 @@ class EmailAddressService {
42
44
  get fallbackEmail() {
43
45
  return this.#getFallbackEmail();
44
46
  }
47
+ /**
48
+ * Get the actual sender address for member emails after DMARC transformation.
49
+ * On managed platforms, this may differ from the configured support address to ensure DMARC compliance.
50
+ */
51
+ getMembersSupportAddress() {
52
+ const configuredAddress = this.#getMembersSupportAddress();
53
+ const transformed = this.getAddressFromString(configuredAddress);
54
+ return transformed.from.address;
55
+ }
45
56
  getAddressFromString(from, replyTo) {
46
57
  const parsedFrom = email_address_parser_js_1.default.parse(from);
47
58
  const parsedReplyTo = replyTo ? email_address_parser_js_1.default.parse(replyTo) : undefined;