ghost 6.24.0 → 6.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/components/tryghost-i18n-6.25.1.tgz +0 -0
  2. package/components/tryghost-parse-email-address-6.25.1.tgz +0 -0
  3. package/core/boot.js +3 -1
  4. package/core/built/admin/assets/{PolarAngleAxis-7TlIQL3R.js → PolarAngleAxis-C6ZeSpZB.js} +1 -1
  5. package/core/built/admin/assets/{_baseAssignValue-DJsCJz6y.js → _baseAssignValue-BKAl5FrV.js} +1 -1
  6. package/core/built/admin/assets/{a-large-small-BnBpzYwl.js → a-large-small-B_ulQCIs.js} +1 -1
  7. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +1 -1
  8. package/core/built/admin/assets/admin-x-settings/{code-editor-view-De3K_osj.mjs → code-editor-view-CBYT1gA1.mjs} +2 -2
  9. package/core/built/admin/assets/admin-x-settings/{index-bGxDzD13.mjs → index-CN5CixaU.mjs} +2 -2
  10. package/core/built/admin/assets/admin-x-settings/{index-5D8KqsFU.mjs → index-CPQODntW.mjs} +2 -2
  11. package/core/built/admin/assets/admin-x-settings/{index-BIUDn7ot.mjs → index-CgTfOd7g.mjs} +9 -9
  12. package/core/built/admin/assets/admin-x-settings/{modals-DuokUyAH.mjs → modals-_y-6HROx.mjs} +2 -2
  13. package/core/built/admin/assets/{at-sign-DC4NtDDM.js → at-sign-B8xWKM0y.js} +1 -1
  14. package/core/built/admin/assets/{audience-CC5U4UxI.js → audience-CBOR--z9.js} +1 -1
  15. package/core/built/admin/assets/{avatar-flipboard-_GQfQXQc.js → avatar-flipboard-IipGb_wZ.js} +1 -1
  16. package/core/built/admin/assets/{bluesky-sharing-BKyzPl9y.js → bluesky-sharing-CkI1QuAm.js} +1 -1
  17. package/core/built/admin/assets/{chart-BCQFlmhJ.js → chart-_slUcr-K.js} +1 -1
  18. package/core/built/admin/assets/{chunk.423.dae9a3bbe7845c3a3cfc.js → chunk.434.de8a15730373eccfeefe.js} +13 -101
  19. package/core/built/admin/assets/{chunk.524.b5e454aa74df4428420c.js → chunk.524.5b9b463506a16221fc91.js} +6 -6
  20. package/core/built/admin/assets/{chunk.582.20635869be68725e6d5c.js → chunk.582.d8eae1661d753befaefd.js} +7 -7
  21. package/core/built/admin/assets/{chunk.123.1255d9e815f933dfdc57.js → chunk.630.3dbb0231a2d871126a80.js} +8 -6
  22. package/core/built/admin/assets/{code-editor-view-BXnjo7qe.js → code-editor-view-BWMqr2JV.js} +1 -1
  23. package/core/built/admin/assets/comments-BeoA0nfK.js +1 -0
  24. package/core/built/admin/assets/{content-helpers-CQXipRWa.js → content-helpers-CmTGdEmL.js} +1 -1
  25. package/core/built/admin/assets/{copy-B-MxRWPu.js → copy-CDHcAPJw.js} +1 -1
  26. package/core/built/admin/assets/{data-list-B2sqn7IE.js → data-list-Crb1ffbp.js} +1 -1
  27. package/core/built/admin/assets/{deleted-feed-item-DQi676Dy.js → deleted-feed-item-DBW0zJQg.js} +1 -1
  28. package/core/built/admin/assets/{dropzone-GgOCfK2j.js → dropzone-B9JUsQlW.js} +3 -3
  29. package/core/built/admin/assets/{edit-profile-COx5FmF9.js → edit-profile-Dz-IhetN.js} +1 -1
  30. package/core/built/admin/assets/{empty-indicator-Bf4Dqk72.js → empty-indicator-2QGnEbtY.js} +1 -1
  31. package/core/built/admin/assets/{en-BkI47Ou-.js → en-BRQS1Z8j.js} +1 -1
  32. package/core/built/admin/assets/{feed-Dzc-8jwd.js → feed-T3kW9UJA.js} +1 -1
  33. package/core/built/admin/assets/filters-ChOMQQZW.js +1 -0
  34. package/core/built/admin/assets/{gh-chart-Ch6dYUac.js → gh-chart-fuWw60Ka.js} +1 -1
  35. package/core/built/admin/assets/{ghost-69e448feab2cb78aba64f7177fa4d6f0.js → ghost-78b4a517ac907213ab47708eef33bc6f.js} +423 -542
  36. package/core/built/admin/assets/{growth-BIBJdcm6.js → growth-D9sCnmon.js} +1 -1
  37. package/core/built/admin/assets/{hash-BGd7HEsT.js → hash-CI1pQ1UC.js} +1 -1
  38. package/core/built/admin/assets/header-OevT95FI.js +1 -0
  39. package/core/built/admin/assets/{inbox-CFzKvW3h.js → inbox-DYY8-M1K.js} +1 -1
  40. package/core/built/admin/assets/{index-Dy8dCc8t.js → index-8CZdLQzu.js} +4 -4
  41. package/core/built/admin/assets/{index-DIO8ceib.js → index-BDAOs7pI.js} +1 -1
  42. package/core/built/admin/assets/{index-BZyG96u1.js → index-BEEgMX-_.js} +1 -1
  43. package/core/built/admin/assets/index-BbveDn8H.css +1 -0
  44. package/core/built/admin/assets/{index-BYXKsmpv.js → index-C-Cpbee5.js} +1 -1
  45. package/core/built/admin/assets/{index-DDJAHYHQ.js → index-CCF2RKxR.js} +12 -12
  46. package/core/built/admin/assets/{index-ZuWSQ3L6.js → index-CEnfjH5h.js} +93 -93
  47. package/core/built/admin/assets/{index-_WMp37yL.js → index-CcFVbffG.js} +1 -1
  48. package/core/built/admin/assets/{index-DpZazR71.js → index-CyMlhfZJ.js} +1 -1
  49. package/core/built/admin/assets/{index-DM98UB_U.js → index-DBZYUfjO.js} +1 -1
  50. package/core/built/admin/assets/{index-D0gRP4Gp.js → index-DJa3pLmx.js} +1 -1
  51. package/core/built/admin/assets/{index-n5opb8tL.js → index-DVqmhe-L.js} +1 -1
  52. package/core/built/admin/assets/{index-S2DGJkmz.js → index-DW5aEj6O.js} +2 -2
  53. package/core/built/admin/assets/{index-CvX5YYEz.js → index-DcFgRUSN.js} +1 -1
  54. package/core/built/admin/assets/{index-7NTHsKgE.js → index-DjgsxHeB.js} +1 -1
  55. package/core/built/admin/assets/index-mqQc8MVd.js +1 -0
  56. package/core/built/admin/assets/{index-CsHjDdYB.js → index-qQZsxgFE.js} +1 -1
  57. package/core/built/admin/assets/{koenig-lexical-8qAZ5o1x.js → koenig-lexical-CsbE6fpi.js} +1 -1
  58. package/core/built/admin/assets/kpis-Dw04ruBN.js +1 -0
  59. package/core/built/admin/assets/{label-CofMdj8x.js → label-tBvn89sm.js} +1 -1
  60. package/core/built/admin/assets/{links-MSlYSad1.js → links-Do4jcjIh.js} +1 -1
  61. package/core/built/admin/assets/{loader-circle-CF5G2g_Z.js → loader-circle-C8exRUku.js} +4 -4
  62. package/core/built/admin/assets/{lucide-react-DZiAu6PQ.js → lucide-react-4VO5Dwow.js} +1 -1
  63. package/core/built/admin/assets/{main-layout-DEVUkqt_.js → main-layout-BAqXRgAx.js} +1 -1
  64. package/core/built/admin/assets/{member-route-mLKS-PsA.js → member-route-D4n-RbQd.js} +1 -1
  65. package/core/built/admin/assets/members-DRLgwCgJ.js +15 -0
  66. package/core/built/admin/assets/{message-square-text-DESy3kSk.js → message-square-text-CC_1bY-Q.js} +5 -5
  67. package/core/built/admin/assets/{minus-DVNwSjD6.js → minus-DSyx9DCe.js} +1 -1
  68. package/core/built/admin/assets/{modals-FYjLOMaQ.js → modals-AryZr_DA.js} +13 -13
  69. package/core/built/admin/assets/{moderation-u4WS46a-.js → moderation-DwKm9LO-.js} +1 -1
  70. package/core/built/admin/assets/{newsletter-Cj0UCg4J.js → newsletter-C0rydPGS.js} +1 -1
  71. package/core/built/admin/assets/{newsletters-_UvhdBE3.js → newsletters-DKdmxfDG.js} +1 -1
  72. package/core/built/admin/assets/{note-CRnFryTp.js → note-996Zos9G.js} +1 -1
  73. package/core/built/admin/assets/{overview-Cr0dGxEU.js → overview-r17hlWAB.js} +1 -1
  74. package/core/built/admin/assets/{pagemenu-BqoDENpI.js → pagemenu-DzpB1AkZ.js} +17 -17
  75. package/core/built/admin/assets/{post-analytics-DIr_rDtL.js → post-analytics-AwufPVFE.js} +1 -1
  76. package/core/built/admin/assets/{post-analytics-context-WA--pwK5.js → post-analytics-context-GFe0lMc-.js} +1 -1
  77. package/core/built/admin/assets/{post-analytics-header-D97SoEaV.js → post-analytics-header-DmGyNvP9.js} +1 -1
  78. package/core/built/admin/assets/{post-share-modal-DBxR7WCX.js → post-share-modal-CTpdCS2X.js} +1 -1
  79. package/core/built/admin/assets/posts/{comments-Bswx2dzs.mjs → comments-Cvt31uBs.mjs} +597 -674
  80. package/core/built/admin/assets/posts/{dialog-67vvNv8e.mjs → dialog-DaViJlnd.mjs} +3 -3
  81. package/core/built/admin/assets/posts/{dropdown-menu-BKZfsAPJ.mjs → dropdown-menu-B4KGMiRI.mjs} +5 -5
  82. package/core/built/admin/assets/posts/{empty-indicator-DTmBQa_3.mjs → empty-indicator-BaD6pCXM.mjs} +2 -2
  83. package/core/built/admin/assets/posts/filters-BsCI4RJg.mjs +2835 -0
  84. package/core/built/admin/assets/posts/{get-site-timezone-D75cHSJ1.mjs → get-site-timezone-Bo-iI2bI.mjs} +2 -2
  85. package/core/built/admin/assets/posts/{growth-BqJWjpAp.mjs → growth-BJxIDp2o.mjs} +14 -14
  86. package/core/built/admin/assets/posts/header-7zhnErbb.mjs +100 -0
  87. package/core/built/admin/assets/posts/{heading-DXxLDQ96.mjs → heading-BCDx_eVF.mjs} +2 -2
  88. package/core/built/admin/assets/posts/{hooks-pVotzebr.mjs → hooks-DQL2O_El.mjs} +2 -2
  89. package/core/built/admin/assets/posts/{index-DZ1HzbGS.mjs → index-0mEgtljt.mjs} +3 -3
  90. package/core/built/admin/assets/posts/{index-By5yPnMZ.mjs → index-BTzJVQ6S.mjs} +10 -10
  91. package/core/built/admin/assets/posts/{input-DdXe2Fjm.mjs → input-DaHXv92f.mjs} +2 -2
  92. package/core/built/admin/assets/posts/{kpis-1MXZeSC9.mjs → kpis-GRsx6fPV.mjs} +10 -10
  93. package/core/built/admin/assets/posts/{links-DGzzdzsH.mjs → links-B1q_pqE9.mjs} +4 -4
  94. package/core/built/admin/assets/posts/{loading-indicator-CSj2spwS.mjs → loading-indicator-DiYqXomj.mjs} +3 -3
  95. package/core/built/admin/assets/posts/{main-layout-CGs25JVH.mjs → main-layout-DeqPTJTe.mjs} +2 -2
  96. package/core/built/admin/assets/posts/{members-BRcED5fL.mjs → members-LABk5yI5.mjs} +4102 -3442
  97. package/core/built/admin/assets/posts/{newsletter-D706LlgM.mjs → newsletter-C_nbHBaP.mjs} +15 -15
  98. package/core/built/admin/assets/posts/{overview-5dG5ypjx.mjs → overview-AJnKv4Ze.mjs} +16 -16
  99. package/core/built/admin/assets/posts/{post-analytics-wvNuOa4c.mjs → post-analytics-b_CKsePp.mjs} +6 -6
  100. package/core/built/admin/assets/posts/{post-analytics-context-BqnFe05q.mjs → post-analytics-context-D6jtpxxm.mjs} +5 -5
  101. package/core/built/admin/assets/posts/{post-analytics-header-Dsi5l6hb.mjs → post-analytics-header-R5UF_tF0.mjs} +11 -11
  102. package/core/built/admin/assets/posts/{post-share-modal-DWe35McY.mjs → post-share-modal-y4BEOajk.mjs} +4 -4
  103. package/core/built/admin/assets/posts/posts-C2hNrf1c.mjs +37 -0
  104. package/core/built/admin/assets/posts/posts.js +1 -1
  105. package/core/built/admin/assets/posts/{reply-B4vwBMwj.mjs → reply-2Uvi3Kxb.mjs} +2 -2
  106. package/core/built/admin/assets/posts/{search-DQuB1WMm.mjs → search-se7meOnL.mjs} +2 -2
  107. package/core/built/admin/assets/posts/{select-BMPHyOdQ.mjs → select-DpUpauFg.mjs} +8 -8
  108. package/core/built/admin/assets/posts/{separator-DgZ2VF9r.mjs → separator-CdBM46dw.mjs} +3 -3
  109. package/core/built/admin/assets/posts/{settings-D-wnf7Qg.mjs → settings-Chhj9xKP.mjs} +2 -2
  110. package/core/built/admin/assets/posts/{sheet-SOdCzxUt.mjs → sheet--k3SiDko.mjs} +3 -3
  111. package/core/built/admin/assets/posts/{skeleton-Du7JXZ_1.mjs → skeleton-CUSLSLoY.mjs} +3 -3
  112. package/core/built/admin/assets/posts/{source-icon-DcBtKbCl.mjs → source-icon-BjYUIoXN.mjs} +3 -3
  113. package/core/built/admin/assets/posts/{stats-CeLav-Hu.mjs → stats-BRdXKaxM.mjs} +4 -4
  114. package/core/built/admin/assets/posts/{table-DlaJMBps.mjs → table-Bgi_UcfL.mjs} +2 -2
  115. package/core/built/admin/assets/posts/{tabs-BQQbtw2g.mjs → tabs-DT8Bh72E.mjs} +14 -14
  116. package/core/built/admin/assets/posts/tags-BCFLnZYl.mjs +399 -0
  117. package/core/built/admin/assets/posts/{tags-CLyaNf39.mjs → tags-Kv2q0o3p.mjs} +2 -2
  118. package/core/built/admin/assets/posts/use-scroll-restoration-CLAUAal3.mjs +517 -0
  119. package/core/built/admin/assets/posts/{use-infinite-virtual-scroll-B_NwiBXK.mjs → virtual-list-window-CQDEqNBL.mjs} +246 -262
  120. package/core/built/admin/assets/posts/{web-TEVKbXSI.mjs → web-CqN0MQU6.mjs} +18 -18
  121. package/core/built/admin/assets/posts-GvXDuhVG.js +1 -0
  122. package/core/built/admin/assets/{referrers-J4vexZCf.js → referrers-Bd9pW-WX.js} +1 -1
  123. package/core/built/admin/assets/{repeat-BxOK9zhN.js → repeat-y6HlMgGO.js} +2 -2
  124. package/core/built/admin/assets/{reply-BoTN-eE0.js → reply-Cc8ryI5v.js} +2 -2
  125. package/core/built/admin/assets/{select-Bd1zcqox.js → select-CGr_5qad.js} +1 -1
  126. package/core/built/admin/assets/{settings-D72z6T_a.js → settings-BPsfSgg3.js} +1 -1
  127. package/core/built/admin/assets/{settings-BGbGoa4s.js → settings-BTd0Adha.js} +19 -19
  128. package/core/built/admin/assets/{sort-button-BOLTpImz.js → sort-button-k76_5HR2.js} +1 -1
  129. package/core/built/admin/assets/{source-icon-BqEM5MvU.js → source-icon-DMt0UskC.js} +1 -1
  130. package/core/built/admin/assets/{sprout-Bf-wc2Vo.js → sprout-Td9eaOoY.js} +2 -2
  131. package/core/built/admin/assets/{square-Cj_bJ0HZ.js → square-Df8vEYlG.js} +1 -1
  132. package/core/built/admin/assets/stats/{audience-BiHYBUzT.mjs → audience-DMTObpL8.mjs} +3 -3
  133. package/core/built/admin/assets/stats/{content-helpers-CIXZiyCm.mjs → content-helpers-ReMp9KJN.mjs} +4 -4
  134. package/core/built/admin/assets/stats/{index-CkKk-p14.mjs → index-6L-yLKFX.mjs} +8 -8
  135. package/core/built/admin/assets/stats/{index-DBB8Oih1.mjs → index-CIUyjeP-.mjs} +5 -5
  136. package/core/built/admin/assets/stats/{index-9zQR4gr0.mjs → index-CMSDpQm3.mjs} +3626 -3513
  137. package/core/built/admin/assets/stats/{index-DErXBfDM.mjs → index-Ca-_KJzy.mjs} +5 -5
  138. package/core/built/admin/assets/stats/{index-DMSmQLxG.mjs → index-CfO5yPSP.mjs} +5 -5
  139. package/core/built/admin/assets/stats/{sort-button-BqY7GFaO.mjs → sort-button-BB_M2CdZ.mjs} +3 -3
  140. package/core/built/admin/assets/stats/{stats-BeJgKX2G.mjs → stats-B2nnuLeU.mjs} +4 -4
  141. package/core/built/admin/assets/stats/stats.js +1 -1
  142. package/core/built/admin/assets/stats/{tabs-CPz357mW.mjs → tabs-DjJG-d3O.mjs} +3 -3
  143. package/core/built/admin/assets/stats/{use-growth-stats-WMYXBQE3.mjs → use-growth-stats-BMT5M7l6.mjs} +3 -3
  144. package/core/built/admin/assets/{stats-DWpHtRym.js → stats-Bs9ZbmXX.js} +1 -1
  145. package/core/built/admin/assets/stats-view-BWoPuaLz.js +1 -0
  146. package/core/built/admin/assets/{step-1-Bjya9Gt0.js → step-1-BUvsuCs0.js} +1 -1
  147. package/core/built/admin/assets/{step-2-CJ6I8JNc.js → step-2-DI9ekXls.js} +1 -1
  148. package/core/built/admin/assets/{step-3-3fs6Y2Kj.js → step-3-DzT_XhaS.js} +2 -2
  149. package/core/built/admin/assets/{table-B4zlzEDP.js → table-BKqaEGAm.js} +1 -1
  150. package/core/built/admin/assets/{tabs-mR5oJGm5.js → tabs-G6Ql8vbf.js} +1 -1
  151. package/core/built/admin/assets/tags-BHm-dlBs.js +1 -0
  152. package/core/built/admin/assets/{tags-CsgMtkFL.js → tags-_taMP0sJ.js} +1 -1
  153. package/core/built/admin/assets/{textarea-CQjEvapR.js → textarea-DcVuGYYq.js} +1 -1
  154. package/core/built/admin/assets/{tiers-CQE0v797.js → tiers-kv-xBoqQ.js} +1 -1
  155. package/core/built/admin/assets/{toggle-group-CR-Z-bJL.js → toggle-group-DsatLdt7.js} +1 -1
  156. package/core/built/admin/assets/{topic-filter-_NyD55eH.js → topic-filter-DkJyIuAf.js} +1 -1
  157. package/core/built/admin/assets/{trash-BFSLkv0z.js → trash-BeZZgbn-.js} +1 -1
  158. package/core/built/admin/assets/{upload-BTxT4v2z.js → upload-BG7KNIn7.js} +4 -4
  159. package/core/built/admin/assets/{use-growth-stats-BGymNu5O.js → use-growth-stats-CNGdl44N.js} +1 -1
  160. package/core/built/admin/assets/use-scroll-restoration-CQ3w0W98.js +1 -0
  161. package/core/built/admin/assets/{use-simple-pagination-Cq7Afp_c.js → use-simple-pagination-BeIJ_5Vv.js} +1 -1
  162. package/core/built/admin/assets/{user-plus-DMl69yKl.js → user-plus-CMxR50-Y.js} +1 -1
  163. package/core/built/admin/assets/{user-round-check-CVuZXUPz.js → user-round-check-2nhPmqP3.js} +1 -1
  164. package/core/built/admin/assets/virtual-list-window-Uq8oKYVG.js +4 -0
  165. package/core/built/admin/assets/{wallet-cards-zHR_y6Sp.js → wallet-cards-CJcVzH0D.js} +1 -1
  166. package/core/built/admin/assets/{web-CmFHgeQw.js → web-BPwTFKrc.js} +1 -1
  167. package/core/built/admin/index.html +7 -7
  168. package/core/frontend/helpers/ghost_head.js +1 -4
  169. package/core/server/data/migrations/versions/6.25/2026-03-26-15-47-00-insert-default-email-design-settings-row.js +42 -0
  170. package/core/server/data/migrations/versions/6.25/2026-03-30-20-22-25-add-email-design-setting-permissions.js +44 -0
  171. package/core/server/data/migrations/versions/6.25/2026-03-30-22-16-43-add-email-design-setting-id-to-automated-emails.js +8 -0
  172. package/core/server/data/migrations/versions/6.25/2026-03-31-13-12-10-backfill-automated-emails-email-design-setting-id.js +45 -0
  173. package/core/server/data/migrations/versions/6.25/2026-03-31-20-31-19-drop-nullable-on-automated-emails-email-design-setting-id.js +3 -0
  174. package/core/server/data/schema/fixtures/fixtures.json +35 -0
  175. package/core/server/data/schema/schema.js +1 -0
  176. package/core/server/data/seeders/importers/comment-reports-importer.js +1 -3
  177. package/core/server/data/seeders/importers/comments-importer.js +3 -2
  178. package/core/server/data/seeders/importers/email-batches-importer.js +3 -3
  179. package/core/server/data/seeders/importers/email-recipients-importer.js +6 -6
  180. package/core/server/data/seeders/importers/emails-importer.js +4 -3
  181. package/core/server/data/seeders/importers/members-click-events-importer.js +1 -1
  182. package/core/server/data/seeders/importers/members-created-events-importer.js +2 -1
  183. package/core/server/data/seeders/importers/members-feedback-importer.js +3 -4
  184. package/core/server/data/seeders/importers/members-login-events-importer.js +3 -2
  185. package/core/server/data/seeders/importers/members-status-events-importer.js +1 -2
  186. package/core/server/data/seeders/importers/members-stripe-customers-importer.js +2 -1
  187. package/core/server/data/seeders/importers/members-stripe-customers-subscriptions-importer.js +4 -2
  188. package/core/server/data/seeders/importers/members-subscribe-events-importer.js +1 -2
  189. package/core/server/data/seeders/importers/members-subscription-created-events-importer.js +3 -1
  190. package/core/server/data/seeders/importers/offer-redemptions-importer.js +3 -3
  191. package/core/server/data/seeders/utils/database-date.js +26 -1
  192. package/core/server/models/automated-email.js +27 -0
  193. package/core/server/models/email-design-setting.js +27 -0
  194. package/core/server/services/members/service.js +0 -22
  195. package/core/server/services/stripe/service.js +1 -2
  196. package/core/server/services/verification/verification-webhook-service.js +2 -2
  197. package/core/server/services/verification/verification-webhook-service.ts +2 -2
  198. package/core/server/services/verification-trigger.js +34 -69
  199. package/core/server/services/webhooks/webhook-trigger.js +1 -1
  200. package/core/shared/labs.js +2 -2
  201. package/package.json +10 -28
  202. package/tsconfig.tsbuildinfo +1 -1
  203. package/yarn.lock +423 -107
  204. package/components/tryghost-i18n-6.24.0.tgz +0 -0
  205. package/components/tryghost-parse-email-address-6.24.0.tgz +0 -0
  206. package/core/built/admin/assets/comments-CteqV7ZN.js +0 -1
  207. package/core/built/admin/assets/filters-DXXaCya7.js +0 -1
  208. package/core/built/admin/assets/index-QB-va_sk.css +0 -1
  209. package/core/built/admin/assets/index-nBUk6L1X.js +0 -1
  210. package/core/built/admin/assets/kpis-Cv75oSPY.js +0 -1
  211. package/core/built/admin/assets/members-DcElgZ2t.js +0 -15
  212. package/core/built/admin/assets/posts/filters-Ds_ZaYIU.mjs +0 -2722
  213. package/core/built/admin/assets/posts/posts-DI5YQFp4.mjs +0 -17
  214. package/core/built/admin/assets/posts/tags-CjwolIiT.mjs +0 -395
  215. package/core/built/admin/assets/posts/use-scroll-restoration-YSR5jwQl.mjs +0 -232
  216. package/core/built/admin/assets/posts-CJHEP4C-.js +0 -1
  217. package/core/built/admin/assets/stats-view-CcyaLPwj.js +0 -1
  218. package/core/built/admin/assets/tags-DEFC6GpD.js +0 -1
  219. package/core/built/admin/assets/use-infinite-virtual-scroll-CNqGwgHd.js +0 -4
  220. package/core/built/admin/assets/use-scroll-restoration-BWtb0NrS.js +0 -1
  221. package/core/shared/config/env/config.testing-browser.json +0 -98
  222. /package/core/built/admin/assets/{chunk.423.dae9a3bbe7845c3a3cfc.js.LICENSE.txt → chunk.434.de8a15730373eccfeefe.js.LICENSE.txt} +0 -0
@@ -50,7 +50,7 @@ class MembersClickEventsImporter extends TableImporter {
50
50
  }
51
51
  this.amount -= 1;
52
52
 
53
- const openedAt = new Date(this.model.opened_at);
53
+ const openedAt = dateToDatabaseString.parse(this.model.opened_at);
54
54
  const laterOn = new Date(openedAt.getTime() + 1000 * 60 * 15);
55
55
  const clickTime = faker.date.between(openedAt.getTime(), laterOn.getTime()); //added getTime here because it threw random errors
56
56
 
@@ -47,7 +47,8 @@ class MembersCreatedEventsImporter extends TableImporter {
47
47
  };
48
48
 
49
49
  if (source === 'member' && luck(30)) {
50
- const post = this.posts.find(p => p.visibility === 'public' && new Date(p.published_at) < new Date(this.model.created_at));
50
+ const memberCreatedAt = dateToDatabaseString.parse(this.model.created_at);
51
+ const post = this.posts.find(p => p.visibility === 'public' && dateToDatabaseString.parse(p.published_at) < memberCreatedAt);
51
52
  if (post) {
52
53
  attribution = {
53
54
  attribution_id: post.id,
@@ -1,5 +1,4 @@
1
1
  const TableImporter = require('./table-importer');
2
- const {faker} = require('@faker-js/faker');
3
2
  const {luck} = require('../utils/random');
4
3
  const dateToDatabaseString = require('../utils/database-date');
5
4
 
@@ -25,10 +24,10 @@ class MembersFeedbackImporter extends TableImporter {
25
24
  return null;
26
25
  }
27
26
 
28
- const openedAt = new Date(this.model.opened_at);
29
- const laterOn = new Date(this.model.opened_at);
27
+ const openedAt = dateToDatabaseString.parse(this.model.opened_at);
28
+ const laterOn = dateToDatabaseString.parse(this.model.opened_at);
30
29
  laterOn.setMinutes(laterOn.getMinutes() + 60);
31
- const feedbackTime = faker.date.between(openedAt, laterOn);
30
+ const feedbackTime = dateToDatabaseString.randomBetween(openedAt, laterOn);
32
31
 
33
32
  const postId = this.emails.find(email => email.id === this.model.email_id).post_id;
34
33
  return {
@@ -35,9 +35,10 @@ class MembersLoginEventsImporter extends TableImporter {
35
35
 
36
36
  setReferencedModel(model) {
37
37
  this.model = model;
38
+ const memberCreatedAt = dateToDatabaseString.parse(model.created_at);
38
39
 
39
40
  const endDate = new Date();
40
- const daysBetween = Math.ceil((endDate.valueOf() - new Date(model.created_at).valueOf()) / (1000 * 60 * 60 * 24));
41
+ const daysBetween = Math.ceil((endDate.valueOf() - memberCreatedAt.valueOf()) / (1000 * 60 * 60 * 24));
41
42
 
42
43
  // Assuming most people either subscribe and lose interest, or maintain steady readership
43
44
  const shape = luck(40) ? 'ease-out' : 'flat';
@@ -47,7 +48,7 @@ class MembersLoginEventsImporter extends TableImporter {
47
48
  // Steady readers login more, readers who lose interest read less overall.
48
49
  // ceil because members will all have logged in at least once
49
50
  total: Math.min(5, shape === 'flat' ? Math.ceil(daysBetween / 3) : Math.ceil(daysBetween / 7)),
50
- startTime: new Date(model.created_at),
51
+ startTime: memberCreatedAt,
51
52
  endTime: endDate
52
53
  });
53
54
  }
@@ -1,5 +1,4 @@
1
1
  const TableImporter = require('./table-importer');
2
- const {faker} = require('@faker-js/faker');
3
2
  const dateToDatabaseString = require('../utils/database-date');
4
3
 
5
4
  class MembersStatusEventsImporter extends TableImporter {
@@ -41,7 +40,7 @@ class MembersStatusEventsImporter extends TableImporter {
41
40
  member_id: model.id,
42
41
  from_status: 'free',
43
42
  to_status: model.status,
44
- created_at: dateToDatabaseString(faker.date.between(new Date(model.created_at), new Date()))
43
+ created_at: dateToDatabaseString(dateToDatabaseString.randomBetween(model.created_at, new Date()))
45
44
  });
46
45
  }
47
46
  }
@@ -1,5 +1,6 @@
1
1
  const {faker} = require('@faker-js/faker');
2
2
  const TableImporter = require('./table-importer');
3
+ const dateToDatabaseString = require('../utils/database-date');
3
4
 
4
5
  class MembersStripeCustomersImporter extends TableImporter {
5
6
  static table = 'members_stripe_customers';
@@ -35,7 +36,7 @@ class MembersStripeCustomersImporter extends TableImporter {
35
36
  // Only 30% of free members should have a stripe customer = have had a subscription in the past or tried to subscribe
36
37
  // The number should increase the older the member is
37
38
 
38
- const daysSinceMemberCreated = Math.floor((new Date() - new Date(this.model.created_at)) / (1000 * 60 * 60 * 24));
39
+ const daysSinceMemberCreated = Math.floor((new Date() - dateToDatabaseString.parse(this.model.created_at)) / (1000 * 60 * 60 * 24));
39
40
  const shouldHaveStripeCustomer = faker.datatype.number({min: 0, max: 100}) < Math.max(Math.min(daysSinceMemberCreated / 60, 15), 2);
40
41
 
41
42
  if (!shouldHaveStripeCustomer) {
@@ -30,6 +30,7 @@ class MembersStripeCustomersSubscriptionsImporter extends TableImporter {
30
30
  this.members = await this.transaction.select('id', 'status', 'created_at').from('members').whereIn('id', membersStripeCustomers.map(m => m.member_id));
31
31
 
32
32
  if (this.members.length === 0) {
33
+ offset += limit;
33
34
  continue;
34
35
  }
35
36
 
@@ -95,6 +96,7 @@ class MembersStripeCustomersSubscriptionsImporter extends TableImporter {
95
96
  (isMonthly ? price.interval === 'month' : price.interval === 'year');
96
97
  });
97
98
  const mrr = createValid ? (isMonthly ? stripePrice.amount : Math.floor(stripePrice.amount / 12)) : 0;
99
+ const memberCreatedAt = dateToDatabaseString.parse(member.created_at);
98
100
 
99
101
  const referenceEndDate = this.lastSubscriptionStart ?? new Date();
100
102
 
@@ -106,7 +108,7 @@ class MembersStripeCustomersSubscriptionsImporter extends TableImporter {
106
108
  }
107
109
  }
108
110
 
109
- if (referenceEndDate < member.created_at) {
111
+ if (referenceEndDate < memberCreatedAt) {
110
112
  // Not possible to create an invalid subscription here
111
113
  return;
112
114
  }
@@ -114,7 +116,7 @@ class MembersStripeCustomersSubscriptionsImporter extends TableImporter {
114
116
  const [startDate] = generateEvents({
115
117
  total: 1,
116
118
  trend: 'negative',
117
- startTime: new Date(member.created_at),
119
+ startTime: memberCreatedAt,
118
120
  endTime: referenceEndDate,
119
121
  shape: 'ease-out'
120
122
  });
@@ -1,5 +1,4 @@
1
1
  const TableImporter = require('./table-importer');
2
- const {faker} = require('@faker-js/faker');
3
2
  const {luck} = require('../utils/random');
4
3
  const dateToDatabaseString = require('../utils/database-date');
5
4
 
@@ -52,7 +51,7 @@ class MembersSubscribeEventsImporter extends TableImporter {
52
51
  return null;
53
52
  }
54
53
 
55
- const createdAt = dateToDatabaseString(faker.date.between(new Date(this.model.created_at), new Date()));
54
+ const createdAt = dateToDatabaseString(dateToDatabaseString.randomBetween(this.model.created_at, new Date()));
56
55
  const newsletterId = this.newsletters[count % this.newsletters.length].id;
57
56
 
58
57
  return {
@@ -1,6 +1,7 @@
1
1
  const TableImporter = require('./table-importer');
2
2
  const {faker} = require('@faker-js/faker');
3
3
  const {luck} = require('../utils/random');
4
+ const dateToDatabaseString = require('../utils/database-date');
4
5
 
5
6
  class MembersSubscriptionCreatedEventsImporter extends TableImporter {
6
7
  static table = 'members_subscription_created_events';
@@ -48,7 +49,8 @@ class MembersSubscriptionCreatedEventsImporter extends TableImporter {
48
49
  };
49
50
 
50
51
  if (luck(30)) {
51
- const post = this.posts.find(p => p.visibility === 'public' && new Date(p.published_at) < new Date(this.model.created_at));
52
+ const createdAt = dateToDatabaseString.parse(this.model.created_at);
53
+ const post = this.posts.find(p => p.visibility === 'public' && dateToDatabaseString.parse(p.published_at) < createdAt);
52
54
  if (post) {
53
55
  attribution = {
54
56
  attribution_id: post.id,
@@ -53,7 +53,7 @@ class OfferRedemptionsImporter extends TableImporter {
53
53
  this.subscriptionPool.push({
54
54
  memberId,
55
55
  subscriptionId: subscription.id,
56
- subscriptionCreatedAt: new Date(subscription.created_at),
56
+ subscriptionCreatedAt: dateToDatabaseString.parse(subscription.created_at),
57
57
  redemptionEndAt: this.getRedemptionEndDate(subscription.current_period_end),
58
58
  availableOffers: [...matchingOffers],
59
59
  lastRedeemedAt: null
@@ -78,7 +78,7 @@ class OfferRedemptionsImporter extends TableImporter {
78
78
 
79
79
  getRedemptionEndDate(currentPeriodEnd) {
80
80
  const now = new Date();
81
- const endDate = currentPeriodEnd ? new Date(currentPeriodEnd) : now;
81
+ const endDate = currentPeriodEnd ? dateToDatabaseString.parse(currentPeriodEnd) : now;
82
82
 
83
83
  return endDate > now ? now : endDate;
84
84
  }
@@ -86,7 +86,7 @@ class OfferRedemptionsImporter extends TableImporter {
86
86
  getCreatedAt(subscriptionState, offer) {
87
87
  const candidateEarliest = new Date(Math.max(
88
88
  subscriptionState.subscriptionCreatedAt.valueOf(),
89
- new Date(offer.created_at).valueOf(),
89
+ dateToDatabaseString.parse(offer.created_at).valueOf(),
90
90
  subscriptionState.lastRedeemedAt ? subscriptionState.lastRedeemedAt.valueOf() + 1000 : 0
91
91
  ));
92
92
  const earliest = new Date(Math.min(
@@ -1,7 +1,32 @@
1
- module.exports = function dateToDatabaseString(date) {
1
+ const {faker} = require('@faker-js/faker');
2
+
3
+ const databaseDatePattern = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?$/;
4
+
5
+ function dateToDatabaseString(date) {
2
6
  if (typeof date === 'string') {
3
7
  // SQLite fix when reusing other dates from the db
4
8
  return date;
5
9
  }
6
10
  return date.toISOString().replace('Z','').replace('T', ' ');
11
+ }
12
+
13
+ dateToDatabaseString.parse = function parseDatabaseDate(date) {
14
+ if (date instanceof Date) {
15
+ return new Date(date);
16
+ }
17
+
18
+ if (typeof date === 'string' && databaseDatePattern.test(date)) {
19
+ return new Date(date.replace(' ', 'T') + 'Z');
20
+ }
21
+
22
+ return new Date(date);
23
+ };
24
+
25
+ dateToDatabaseString.randomBetween = function randomBetween(start, end) {
26
+ const earliest = dateToDatabaseString.parse(start);
27
+ const latest = dateToDatabaseString.parse(end);
28
+
29
+ return latest > earliest ? faker.date.between(earliest, latest) : earliest;
7
30
  };
31
+
32
+ module.exports = dateToDatabaseString;
@@ -1,10 +1,12 @@
1
1
  const ghostBookshelf = require('./base');
2
+ const errors = require('@tryghost/errors');
2
3
  const logging = require('@tryghost/logging');
3
4
  const urlUtils = require('../../shared/url-utils');
4
5
  const lexicalLib = require('../lib/lexical');
5
6
  const {MEMBER_WELCOME_EMAIL_SLUGS} = require('../services/member-welcome-emails/constants');
6
7
 
7
8
  const MEMBER_WELCOME_EMAIL_SLUG_SET = new Set(Object.values(MEMBER_WELCOME_EMAIL_SLUGS));
9
+ const DEFAULT_EMAIL_DESIGN_SETTING_SLUG = 'default-automated-email';
8
10
 
9
11
  const AutomatedEmail = ghostBookshelf.Model.extend({
10
12
  tableName: 'automated_emails',
@@ -15,6 +17,13 @@ const AutomatedEmail = ghostBookshelf.Model.extend({
15
17
  };
16
18
  },
17
19
 
20
+ /**
21
+ * @returns {import('bookshelf').Model}
22
+ */
23
+ emailDesignSetting() {
24
+ return this.belongsTo('EmailDesignSetting', 'email_design_setting_id', 'id');
25
+ },
26
+
18
27
  parse() {
19
28
  const attrs = ghostBookshelf.Model.prototype.parse.apply(this, arguments);
20
29
 
@@ -26,6 +35,24 @@ const AutomatedEmail = ghostBookshelf.Model.extend({
26
35
  return attrs;
27
36
  },
28
37
 
38
+ async onCreating(model, attrs, options) {
39
+ if (!model.get('email_design_setting_id')) {
40
+ const emailDesignSetting = await ghostBookshelf.model('EmailDesignSetting').findOne({
41
+ slug: DEFAULT_EMAIL_DESIGN_SETTING_SLUG
42
+ }, options);
43
+
44
+ if (!emailDesignSetting) {
45
+ throw new errors.InternalServerError({
46
+ message: 'Missing default email design setting for automated emails'
47
+ });
48
+ }
49
+
50
+ model.set('email_design_setting_id', emailDesignSetting.get('id'));
51
+ }
52
+
53
+ return ghostBookshelf.Model.prototype.onCreating.call(this, model, attrs, options);
54
+ },
55
+
29
56
  // Alternative to Bookshelf's .format() that is only called when writing to db
30
57
  formatOnWrite(attrs) {
31
58
  // Ensure lexical URLs are stored as transform-ready with __GHOST_URL__ representing config.url
@@ -0,0 +1,27 @@
1
+ const ghostBookshelf = require('./base');
2
+
3
+ const EmailDesignSetting = ghostBookshelf.Model.extend({
4
+ tableName: 'email_design_settings',
5
+
6
+ defaults() {
7
+ return {
8
+ background_color: 'light',
9
+ header_background_color: 'transparent',
10
+ show_header_title: true,
11
+ button_color: 'accent',
12
+ button_corners: 'rounded',
13
+ button_style: 'fill',
14
+ link_color: 'accent',
15
+ link_style: 'underline',
16
+ body_font_category: 'sans_serif',
17
+ title_font_category: 'sans_serif',
18
+ title_font_weight: 'bold',
19
+ image_corners: 'square',
20
+ show_badge: true
21
+ };
22
+ }
23
+ });
24
+
25
+ module.exports = {
26
+ EmailDesignSetting: ghostBookshelf.model('EmailDesignSetting', EmailDesignSetting)
27
+ };
@@ -45,26 +45,6 @@ const membersStats = new MembersStats({
45
45
  let membersApi;
46
46
  let verificationTrigger;
47
47
 
48
- const sendVerificationEmail = async ({subject, message, amountTriggered}) => {
49
- const escalationAddress = config.get('hostSettings:emailVerification:escalationAddress');
50
- const replyTo = config.get('user_email');
51
- const fromAddress = settingsHelpers.getDefaultEmailAddress();
52
-
53
- if (escalationAddress) {
54
- await ghostMailer.send({
55
- subject,
56
- html: tpl(message, {
57
- amountTriggered: amountTriggered,
58
- siteUrl: urlUtils.getSiteUrl()
59
- }),
60
- forceTextContent: true,
61
- from: fromAddress,
62
- replyTo,
63
- to: escalationAddress
64
- });
65
- }
66
- };
67
-
68
48
  const initMembersCSVImporter = ({stripeAPIService}) => {
69
49
  return makeMembersCSVImporter({
70
50
  storagePath: config.getContentPath('data'),
@@ -113,8 +93,6 @@ const initVerificationTrigger = () => {
113
93
  isVerified: () => config.get('hostSettings:emailVerification:verified') === true,
114
94
  isVerificationRequired: () => settingsCache.get('email_verification_required') === true,
115
95
  setVerificationRequired: value => settingsCache.set('email_verification_required', {value}),
116
- isVerificationFlowEnabled: () => labsService.isSet('verificationFlow'),
117
- sendVerificationEmail,
118
96
  sendVerificationWebhook: verificationWebhookService.sendVerificationWebhook.bind(verificationWebhookService),
119
97
  membersStats,
120
98
  Settings: models.Settings,
@@ -17,8 +17,7 @@ const settingsCache = require('../../../shared/settings-cache');
17
17
  async function configureApi() {
18
18
  const cfg = getConfig({settingsHelpers, config, urlUtils});
19
19
  if (cfg) {
20
- // @NOTE: to not start test mode when running playwright suite
21
- cfg.testEnv = process.env.NODE_ENV.startsWith('test') && process.env.NODE_ENV !== 'testing-browser';
20
+ cfg.testEnv = process.env.NODE_ENV.startsWith('test');
22
21
  await module.exports.configure(cfg);
23
22
  return true;
24
23
  }
@@ -47,11 +47,11 @@ class VerificationWebhookService {
47
47
  async sendVerificationWebhook({ amountTriggered, threshold, method }) {
48
48
  const { webhookType, webhookUrl, webhookSecret, siteId } = this.#readWebhookConfig();
49
49
  if (typeof webhookUrl !== 'string' || webhookUrl.length === 0) {
50
- this.#logging.warn('Verification webhook flow is enabled but webhookUrl is not configured.');
50
+ this.#logging.warn('Verification webhook is not configured because webhookUrl is missing.');
51
51
  return false;
52
52
  }
53
53
  if (typeof webhookType !== 'string' || webhookType.length === 0) {
54
- this.#logging.warn('Verification webhook flow is enabled but webhookType is not configured.');
54
+ this.#logging.warn('Verification webhook is not configured because webhookType is missing.');
55
55
  return false;
56
56
  }
57
57
  const payload = {
@@ -78,12 +78,12 @@ export class VerificationWebhookService {
78
78
  const {webhookType, webhookUrl, webhookSecret, siteId} = this.#readWebhookConfig();
79
79
 
80
80
  if (typeof webhookUrl !== 'string' || webhookUrl.length === 0) {
81
- this.#logging.warn('Verification webhook flow is enabled but webhookUrl is not configured.');
81
+ this.#logging.warn('Verification webhook is not configured because webhookUrl is missing.');
82
82
  return false;
83
83
  }
84
84
 
85
85
  if (typeof webhookType !== 'string' || webhookType.length === 0) {
86
- this.#logging.warn('Verification webhook flow is enabled but webhookType is not configured.');
86
+ this.#logging.warn('Verification webhook is not configured because webhookType is missing.');
87
87
  return false;
88
88
  }
89
89
 
@@ -3,17 +3,7 @@ const DomainEvents = require('@tryghost/domain-events');
3
3
  const {MemberCreatedEvent} = require('../../shared/events');
4
4
 
5
5
  const messages = {
6
- emailVerificationNeeded: `We're hard at work processing your import. To make sure you get great deliverability, we'll need to enable some extra features for your account. A member of our team will be in touch with you by email to review your account make sure everything is configured correctly so you're ready to go.`,
7
- emailVerificationEmailSubject: `Email needs verification`,
8
- emailVerificationEmailMessageImport: `Email verification needed for site: {siteUrl}, has imported: {amountTriggered} members in the last 30 days.`,
9
- emailVerificationEmailMessageAdmin: `Email verification needed for site: {siteUrl} has added: {amountTriggered} members through the Admin client in the last 30 days.`,
10
- emailVerificationEmailMessageAPI: `Email verification needed for site: {siteUrl} has added: {amountTriggered} members through the API in the last 30 days.`
11
- };
12
-
13
- const verificationMessageBySource = {
14
- api: messages.emailVerificationEmailMessageAPI,
15
- admin: messages.emailVerificationEmailMessageAdmin,
16
- import: messages.emailVerificationEmailMessageImport
6
+ emailVerificationNeeded: `We're hard at work processing your import. To make sure you get great deliverability, we'll need to enable some extra features for your account. A member of our team will be in touch by email to review your account and make sure everything is configured correctly so you're ready to go.`
17
7
  };
18
8
 
19
9
  class VerificationTrigger {
@@ -26,8 +16,6 @@ class VerificationTrigger {
26
16
  * @param {() => boolean} deps.isVerified Check Ghost config to see if we are already verified
27
17
  * @param {() => boolean} deps.isVerificationRequired Check Ghost settings to see whether verification has been requested
28
18
  * @param {(value: boolean) => void} deps.setVerificationRequired Directly update the settings cache for email_verification_required
29
- * @param {() => boolean} deps.isVerificationFlowEnabled Check whether webhook-based verification flow is enabled
30
- * @param {(content: {subject: string, message: string, amountTriggered: number}) => Promise<void>} deps.sendVerificationEmail Sends an email to the escalation address to confirm that customer needs to be verified
31
19
  * @param {(content: {amountTriggered: number, threshold: number, method: string}) => Promise<boolean>} deps.sendVerificationWebhook Sends a webhook to the escalation service to confirm that customer needs to be verified
32
20
  * @param {any} deps.Settings Ghost Settings model
33
21
  * @param {any} deps.eventRepository For querying events
@@ -39,8 +27,6 @@ class VerificationTrigger {
39
27
  isVerified,
40
28
  isVerificationRequired,
41
29
  setVerificationRequired,
42
- isVerificationFlowEnabled,
43
- sendVerificationEmail,
44
30
  sendVerificationWebhook,
45
31
  Settings,
46
32
  eventRepository
@@ -51,9 +37,7 @@ class VerificationTrigger {
51
37
  this._isVerified = isVerified;
52
38
  this._isVerificationRequired = isVerificationRequired;
53
39
  this._setVerificationRequired = setVerificationRequired || (() => {});
54
- this._isVerificationFlowEnabled = isVerificationFlowEnabled || (() => false);
55
- this._sendVerificationEmail = sendVerificationEmail;
56
- this._sendVerificationWebhook = sendVerificationWebhook;
40
+ this._sendVerificationWebhook = sendVerificationWebhook || (async () => false);
57
41
  this._Settings = Settings;
58
42
  this._eventRepository = eventRepository;
59
43
 
@@ -76,13 +60,9 @@ class VerificationTrigger {
76
60
  return this._getImportTriggerThreshold();
77
61
  }
78
62
 
79
- _shouldUseWebhookFlow() {
80
- return this._isVerificationFlowEnabled() && typeof this._sendVerificationWebhook === 'function';
81
- }
82
-
83
63
  /**
84
64
  *
85
- * @param {MemberCreatedEvent} event
65
+ * @param {InstanceType<typeof MemberCreatedEvent>} event
86
66
  */
87
67
  async _handleMemberCreatedEvent(event) {
88
68
  const source = event.data?.source;
@@ -94,25 +74,29 @@ class VerificationTrigger {
94
74
  sourceThreshold = this._adminTriggerThreshold;
95
75
  }
96
76
 
97
- if (['api', 'admin'].includes(source) && isFinite(sourceThreshold)) {
77
+ if (['api', 'admin'].includes(source) && Number.isFinite(sourceThreshold)) {
98
78
  const createdAt = new Date();
99
79
  createdAt.setDate(createdAt.getDate() - 30);
100
80
  const events = await this._eventRepository.getSignupEvents({}, {
101
- source: source,
81
+ source,
102
82
  created_at: {
103
83
  $gt: createdAt.toISOString().replace('T', ' ').substring(0, 19)
104
84
  }
105
85
  });
106
86
 
87
+ // TODO: Fix off-by-one issue in event dispatch: https://linear.app/ghost/issue/BER-3507/off-by-one-errors-in-event-query-pagination
88
+ const addOneForCurrentEvent = events.meta.pagination.total < events.meta.pagination.limit && events.data.length !== events.meta.pagination.total;
89
+ const currentImport = events.meta.pagination.total + (addOneForCurrentEvent ? 1 : 0);
90
+
107
91
  const membersTotal = (await this._eventRepository.getSignupEvents({}, {
108
92
  source: 'member'
109
93
  })).meta.pagination.total;
110
94
 
111
95
  const effectiveThreshold = Math.max(sourceThreshold, membersTotal);
112
96
 
113
- if (events.meta.pagination.total > effectiveThreshold) {
97
+ if (currentImport > effectiveThreshold) {
114
98
  await this._startVerificationProcess({
115
- amount: events.meta.pagination.total,
99
+ amount: currentImport,
116
100
  threshold: effectiveThreshold,
117
101
  method: source,
118
102
  throwOnTrigger: false,
@@ -148,24 +132,9 @@ class VerificationTrigger {
148
132
  };
149
133
  }
150
134
 
151
- async _startLegacyEmailVerificationProcess({amount, triggerSource, throwOnTrigger}) {
152
- // GA removal point: delete this method once webhook delivery fully replaces email escalation.
153
- const verificationMessage = verificationMessageBySource[triggerSource] || messages.emailVerificationEmailMessageImport;
154
-
155
- await this._markVerificationRequired();
156
-
157
- await this._sendVerificationEmail({
158
- message: verificationMessage,
159
- subject: messages.emailVerificationEmailSubject,
160
- amountTriggered: amount
161
- });
162
-
163
- return this._finishTrigger(throwOnTrigger);
164
- }
165
-
166
135
  async getImportThreshold() {
167
136
  const volumeThreshold = this._importTriggerThreshold;
168
- if (!isFinite(volumeThreshold)) {
137
+ if (!Number.isFinite(volumeThreshold)) {
169
138
  return volumeThreshold;
170
139
  }
171
140
 
@@ -186,7 +155,7 @@ class VerificationTrigger {
186
155
  }
187
156
 
188
157
  async testImportThreshold() {
189
- if (!isFinite(this._importTriggerThreshold)) {
158
+ if (!Number.isFinite(this._importTriggerThreshold)) {
190
159
  // Infinite threshold, quick path
191
160
  return;
192
161
  }
@@ -210,16 +179,18 @@ class VerificationTrigger {
210
179
  }
211
180
  });
212
181
 
182
+ const currentImport = events.meta.pagination.total;
183
+
213
184
  const membersTotal = (await this._eventRepository.getSignupEvents({}, {
214
185
  source: 'member'
215
186
  })).meta.pagination.total;
216
187
 
217
188
  // Import threshold is either the total number of members (discounting any created by imports in
218
189
  // the last 30 days) or the threshold defined in config, whichever is greater.
219
- const importThreshold = Math.max(membersTotal - events.meta.pagination.total, this._importTriggerThreshold);
220
- if (isFinite(importThreshold) && events.meta.pagination.total > importThreshold) {
190
+ const importThreshold = Math.max(membersTotal - currentImport, this._importTriggerThreshold);
191
+ if (Number.isFinite(importThreshold) && currentImport > importThreshold) {
221
192
  await this._startVerificationProcess({
222
- amount: events.meta.pagination.total,
193
+ amount: currentImport,
223
194
  threshold: importThreshold,
224
195
  method: 'import',
225
196
  throwOnTrigger: false,
@@ -254,36 +225,30 @@ class VerificationTrigger {
254
225
  return {needsVerification: false};
255
226
  }
256
227
 
257
- // Only trigger flag change and escalation notification the first time
228
+ // Only trigger verification once.
258
229
  if (this._isVerificationRequired()) {
259
230
  return {needsVerification: false};
260
231
  }
261
232
 
262
- const triggerSource = method ?? source ?? 'import';
233
+ let webhookWasSent = false;
263
234
 
264
- if (this._shouldUseWebhookFlow()) {
265
- try {
266
- const webhookWasSent = await this._sendVerificationWebhook({
267
- amountTriggered: amount,
268
- threshold: threshold ?? amount,
269
- method: triggerSource
270
- });
235
+ try {
236
+ webhookWasSent = await this._sendVerificationWebhook({
237
+ amountTriggered: amount,
238
+ threshold: threshold ?? amount,
239
+ method: method ?? source ?? 'import'
240
+ });
241
+ } catch (error) {
242
+ // `sendVerificationWebhook` already logs delivery failures.
243
+ return {needsVerification: false};
244
+ }
271
245
 
272
- if (webhookWasSent) {
273
- await this._markVerificationRequired();
274
- return this._finishTrigger(throwOnTrigger);
275
- }
276
- } catch (error) {
277
- // `sendVerificationWebhook` already logs delivery failures.
278
- }
246
+ if (!webhookWasSent) {
247
+ return {needsVerification: false};
279
248
  }
280
249
 
281
- // Default email flow — used unless the webhook flow is enabled and succeeds.
282
- return await this._startLegacyEmailVerificationProcess({
283
- amount,
284
- triggerSource,
285
- throwOnTrigger
286
- });
250
+ await this._markVerificationRequired();
251
+ return this._finishTrigger(throwOnTrigger);
287
252
  }
288
253
  }
289
254
 
@@ -91,7 +91,7 @@ class WebhookTrigger {
91
91
  error: `Request failed: ${err.code || 'unknown'}`
92
92
  });
93
93
 
94
- logging.warn(`Request to ${webhook.get('target_url') || null} failed because of: ${err.code || ''}.`);
94
+ logging.error(`[WEBHOOK_DELIVERY_FAILURE] url=${webhook.get('target_url') || 'unknown'} status=${err.statusCode || 'none'} error_code=${err.code || 'unknown'} message=${err.message || ''}`, err);
95
95
  };
96
96
  }
97
97
 
@@ -47,11 +47,11 @@ const PRIVATE_FEATURES = [
47
47
  'emailUniqueid',
48
48
  'themeTranslation',
49
49
  'indexnow',
50
- 'verificationFlow',
51
50
  'membersForward',
52
51
  'welcomeEmailsDesignCustomization',
53
52
  'pictureImageFormats',
54
- 'smarterCounts'
53
+ 'smarterCounts',
54
+ 'giftSubscriptions'
55
55
  ];
56
56
 
57
57
  module.exports.GA_KEYS = [...GA_FEATURES];