ghost 6.62.0 → 6.63.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 (439) hide show
  1. package/components/tryghost-adapter-base-jobs-0.0.0.tgz +0 -0
  2. package/components/tryghost-adapter-base-scheduling-0.2.3.tgz +0 -0
  3. package/components/tryghost-adapter-base-sso-0.1.3.tgz +0 -0
  4. package/components/tryghost-admin-api-schema-0.0.0.tgz +0 -0
  5. package/components/tryghost-checkout-0.0.0.tgz +0 -0
  6. package/components/tryghost-i18n-0.0.0.tgz +0 -0
  7. package/components/tryghost-kg-card-factory-5.2.4.tgz +0 -0
  8. package/components/tryghost-kg-clean-basic-html-4.3.4.tgz +0 -0
  9. package/components/tryghost-kg-converters-1.2.5.tgz +0 -0
  10. package/components/tryghost-kg-default-cards-10.3.5.tgz +0 -0
  11. package/components/tryghost-kg-default-nodes-2.2.1.tgz +0 -0
  12. package/components/tryghost-kg-default-transforms-1.3.4.tgz +0 -0
  13. package/components/tryghost-kg-html-to-lexical-1.4.0.tgz +0 -0
  14. package/components/tryghost-kg-lexical-html-renderer-1.5.0.tgz +0 -0
  15. package/components/tryghost-kg-markdown-html-renderer-7.2.4.tgz +0 -0
  16. package/components/tryghost-kg-utils-1.1.4.tgz +0 -0
  17. package/components/tryghost-limit-service-0.0.0.tgz +0 -0
  18. package/components/tryghost-metafield-types-0.0.0.tgz +0 -0
  19. package/content/themes/source/package.json +2 -2
  20. package/content/themes/source/partials/post-card.hbs +1 -1
  21. package/core/boot.js +7 -5
  22. package/core/built/admin/assets/{about-Dyae_UT6.js → about-C1C3s5-g.js} +1 -1
  23. package/core/built/admin/assets/accordion-CxGsDKwa.js +1 -0
  24. package/core/built/admin/assets/{account-migration-CESa0mnX.js → account-migration-CbUhW6gk.js} +1 -1
  25. package/core/built/admin/assets/activitypub/{account-migration-By69CgCp.js → account-migration-KA6Jjf9L.js} +7 -7
  26. package/core/built/admin/assets/activitypub/activitypub.js +3 -3
  27. package/core/built/admin/assets/activitypub/{avatar-DLPL3Abs.js → avatar-DVJF5bxP.js} +2 -2
  28. package/core/built/admin/assets/activitypub/{avatar-flipboard-C9iDY-QV.js → avatar-flipboard-csQaNeYK.js} +2 -2
  29. package/core/built/admin/assets/activitypub/{bluesky-sharing-BJcPMrDd.js → bluesky-sharing-C7nTDMvw.js} +9 -9
  30. package/core/built/admin/assets/activitypub/{content-formatters-3rTHg3mp.js → content-formatters-CLcjaBwV.js} +4 -4
  31. package/core/built/admin/assets/activitypub/{copy-DIAOQoCZ.js → copy-DcOh8kXw.js} +2 -2
  32. package/core/built/admin/assets/activitypub/{domain-DUR4xSkV.js → domain-fZ-X8i4-.js} +9 -9
  33. package/core/built/admin/assets/activitypub/{edit-profile-BqtfzxCp.js → edit-profile-BqMLQV88.js} +6 -6
  34. package/core/built/admin/assets/activitypub/{explore-BxuJd-J4.js → explore-B4yPoP8E.js} +10 -10
  35. package/core/built/admin/assets/activitypub/{feed-BM1TcksA.js → feed-BRC_mvAe.js} +9 -9
  36. package/core/built/admin/assets/activitypub/{field-Dn7HHx2-.js → field-COacNgOt.js} +3 -3
  37. package/core/built/admin/assets/activitypub/{hash-Bm3WxYN4.js → hash-C7u-irQQ.js} +2 -2
  38. package/core/built/admin/assets/activitypub/{inbox-CCjkuow2.js → inbox-BW-yvUoN.js} +10 -10
  39. package/core/built/admin/assets/activitypub/{inbox-d8oJRTf4.js → inbox-CbTTdDQ3.js} +2 -2
  40. package/core/built/admin/assets/activitypub/{label-1AA_s9Fa.js → label-kPMmYI7l.js} +3 -3
  41. package/core/built/admin/assets/activitypub/layout-BL4_jtuV.js +1 -0
  42. package/core/built/admin/assets/activitypub/{moderation-CnmxEB8w.js → moderation-CuidJzc0.js} +10 -10
  43. package/core/built/admin/assets/activitypub/{note-ZKc_Bnba.js → note-CAMRRSqD.js} +7 -7
  44. package/core/built/admin/assets/activitypub/{notifications-CB2tJZau.js → notifications-BmNlXGgp.js} +9 -9
  45. package/core/built/admin/assets/activitypub/{onboarding-BsJ9E5qf.js → onboarding-Bqg7fh_L.js} +2 -2
  46. package/core/built/admin/assets/activitypub/{onboarding-CSUJJIlh.js → onboarding-Dmun9jr_.js} +2 -2
  47. package/core/built/admin/assets/activitypub/{preferences-0-JZnUkI.js → preferences--oH19wf9.js} +11 -11
  48. package/core/built/admin/assets/activitypub/{profile-C35cFIhM.js → profile-DH_tQx2D.js} +14 -14
  49. package/core/built/admin/assets/activitypub/{reply-DYK5vVqb.js → reply-Cap9PxSH.js} +2 -2
  50. package/core/built/admin/assets/activitypub/{routes-BD4vscyc.js → routes-DdyY9uFF.js} +30 -30
  51. package/core/built/admin/assets/activitypub/{separator-BbQqTm-p.js → separator-CAxxu8VV.js} +3 -3
  52. package/core/built/admin/assets/activitypub/{settings-BsMROdqh.js → settings-B6EEcVkW.js} +6 -6
  53. package/core/built/admin/assets/activitypub/{site-O0RL9YCN.js → site-BYps63oh.js} +2 -2
  54. package/core/built/admin/assets/activitypub/{step-1-BMSx7c_X.js → step-1-CKjR2KAm.js} +6 -6
  55. package/core/built/admin/assets/activitypub/{step-2-DtPQzlTH.js → step-2-B6PKfOUQ.js} +6 -6
  56. package/core/built/admin/assets/activitypub/{step-3-CLlOZTyt.js → step-3-C-qBbcUo.js} +11 -11
  57. package/core/built/admin/assets/activitypub/{tabs-Dxk-b9M-.js → tabs-DLhZtVde.js} +6 -6
  58. package/core/built/admin/assets/activitypub/{topic-filter-5yZH6tId.js → topic-filter-DsUOuu0k.js} +3 -3
  59. package/core/built/admin/assets/activitypub/{use-activity-pub-queries-CV50qe03.js → use-activity-pub-queries-CX9snsLs.js} +3 -3
  60. package/core/built/admin/assets/activitypub/{use-navigate-with-base-path-Bmk5h4lW.js → use-navigate-with-base-path-DsJTGDSS.js} +988 -959
  61. package/core/built/admin/assets/activitypub/{use-reply-chain-data-Cbvzgjzd.js → use-reply-chain-data-AJi9zkzm.js} +5 -5
  62. package/core/built/admin/assets/activitypub/{users-BjcoP_HC.js → users-inweyKL5.js} +186 -160
  63. package/core/built/admin/assets/activitypub/{x-DbQZOLiI.js → x-DR8AT3-4.js} +2 -2
  64. package/core/built/admin/assets/{add-integration-modal-BlF6rs_f.js → add-integration-modal-BT_onDJy.js} +1 -1
  65. package/core/built/admin/assets/{add-newsletter-modal-CglVQqah.js → add-newsletter-modal-Cx1kQpiY.js} +1 -1
  66. package/core/built/admin/assets/{add-recommendation-modal-wz7BsuaG.js → add-recommendation-modal-B5nvAhvr.js} +1 -1
  67. package/core/built/admin/assets/{analytics-C4HE1gBg.js → analytics-DjoJWPI7.js} +1 -1
  68. package/core/built/admin/assets/{announcement-bar-modal-Ci4zz8jO.js → announcement-bar-modal-Dwd0EGFP.js} +1 -1
  69. package/core/built/admin/assets/{api-keys-3-0vJC3Y.js → api-keys-BGPzLHzA.js} +1 -1
  70. package/core/built/admin/assets/{api-keys-Ck-Yteiv.js → api-keys-BoeacM-s.js} +1 -1
  71. package/core/built/admin/assets/{audience-CtbA54Sn.js → audience-ceV3XVHq.js} +1 -1
  72. package/core/built/admin/assets/automated-emails-BLesE41L.js +1 -0
  73. package/core/built/admin/assets/{automations-sAcJ1334.js → automations-BN4u3GR8.js} +1 -1
  74. package/core/built/admin/assets/automations-C-ZTUwW3.js +1 -0
  75. package/core/built/admin/assets/{bluesky-sharing-BEtJ_gL_.js → bluesky-sharing-vzh-oPXL.js} +1 -1
  76. package/core/built/admin/assets/{breadcrumb-zUrZrBtW.js → breadcrumb-CaLk2b7D.js} +1 -1
  77. package/core/built/admin/assets/{button-eNMKQWXr.js → button-Dvcv1QSR.js} +1 -1
  78. package/core/built/admin/assets/calendar-Bl9RLcZX.js +1 -0
  79. package/core/built/admin/assets/{chart-helpers-CfBKt0vT.js → chart-helpers-FHywU0pj.js} +1 -1
  80. package/core/built/admin/assets/{checkbox-CgOn_tKW.js → checkbox-BtayFtjg.js} +1 -1
  81. package/core/built/admin/assets/chunk.254.035b85ec456ebbd7336b.js +440 -0
  82. package/core/built/admin/assets/{chunk.524.a697dee88db67fdf6021.js → chunk.524.e6975095fccdf22d90f8.js} +5 -5
  83. package/core/built/admin/assets/{chunk.582.5ed5d124cfca9c259b9c.js → chunk.582.98b485f9b2802f540897.js} +6 -6
  84. package/core/built/admin/assets/{chunk.492.f24d855d7e3f3aad261c.js → chunk.901.4ccc639821020230bcca.js} +9949 -7921
  85. package/core/built/admin/assets/{code-editor-view-K2wI-I5f.js → code-editor-view-C3ZfoF1X.js} +1 -1
  86. package/core/built/admin/assets/{code-editor-view-BYNoAoNV.js → code-editor-view-CAyRWy1s.js} +1 -1
  87. package/core/built/admin/assets/{color-picker-Ddw80xX6.js → color-picker-G4kGR7rp.js} +1 -1
  88. package/core/built/admin/assets/{color-picker-field-D9nUhvA7.js → color-picker-field-DQFVTNnT.js} +1 -1
  89. package/core/built/admin/assets/{combobox-w9j8PPQT.js → combobox-BnYzgEB4.js} +1 -1
  90. package/core/built/admin/assets/{command-BfpHORBv.js → command-C_vyTikI.js} +1 -1
  91. package/core/built/admin/assets/comments-aBrxfnZf.js +1 -0
  92. package/core/built/admin/assets/{config-DOMQDZGZ.js → config-kSHvwdTG.js} +1 -1
  93. package/core/built/admin/assets/{confirmation-context-CBM8G0Vj.js → confirmation-context-DwXX_Y5h.js} +1 -1
  94. package/core/built/admin/assets/{content-api-modal-TXRz3U7S.js → content-api-modal-D2G0nJP6.js} +1 -1
  95. package/core/built/admin/assets/{context-menu-_QMdd3kp.js → context-menu-DOVB6yaz.js} +1 -1
  96. package/core/built/admin/assets/{copy-field-6N01S07E.js → copy-field-DCJwh9qf.js} +1 -1
  97. package/core/built/admin/assets/{create-ghost-browse-value-source-qIBymvvf.js → create-ghost-browse-value-source-5C53jJaX.js} +1 -1
  98. package/core/built/admin/assets/custom-field-type-option-BAuW0F-E.js +1 -0
  99. package/core/built/admin/assets/{custom-integration-modal-DmNquxUV.js → custom-integration-modal-AbJGVyKc.js} +1 -1
  100. package/core/built/admin/assets/{date-range-select-uvz_DtPh.js → date-range-select-BY3K-ukj.js} +1 -1
  101. package/core/built/admin/assets/design-and-theme-modal-BvEy4_pC.js +2 -0
  102. package/core/built/admin/assets/{dirty-confirm-dialog-BZ1iytEd.js → dirty-confirm-dialog-DVPuGz3U.js} +1 -1
  103. package/core/built/admin/assets/{disabled-sources-indicator-DMRrxsiJ.js → disabled-sources-indicator-Cy7Zp4c8.js} +1 -1
  104. package/core/built/admin/assets/{domain-BNYrt0aD.js → domain-Dl61QGuk.js} +1 -1
  105. package/core/built/admin/assets/{dropzone-D7nFdAe5.js → dropzone-Dtz5Rmjt.js} +4 -4
  106. package/core/built/admin/assets/edit-profile-DNJSMDwm.js +1 -0
  107. package/core/built/admin/assets/editor-DbAen9zf.js +8 -0
  108. package/core/built/admin/assets/editor-screen-BFWThIai.js +26 -0
  109. package/core/built/admin/assets/email-sending-status-context-CWDOD1FN.js +1 -0
  110. package/core/built/admin/assets/email-sending-status-provider-DCPkDICt.js +1 -0
  111. package/core/built/admin/assets/emails-vFMk4yYl.js +1 -0
  112. package/core/built/admin/assets/{embed-signup-form-modal-Ds6mPEi5.js → embed-signup-form-modal-DGqsORxS.js} +1 -1
  113. package/core/built/admin/assets/{explore-UY10Egsp.js → explore-nnRr1Fja.js} +1 -1
  114. package/core/built/admin/assets/feature-flag-overrides-context-DQjdJFAk.js +1 -0
  115. package/core/built/admin/assets/{feature-image-placeholder-D1iF_8bc.js → feature-image-placeholder-BrIHmPs8.js} +1 -1
  116. package/core/built/admin/assets/{feed-81C8mJWR.js → feed-BNUcNu-W.js} +1 -1
  117. package/core/built/admin/assets/feedback-DJ4Zb9rZ.js +1 -0
  118. package/core/built/admin/assets/{field-Dihp5p18.js → field-C93_Mg6j.js} +1 -1
  119. package/core/built/admin/assets/filter-sources-CGJb4C5r.js +1 -0
  120. package/core/built/admin/assets/{filters-DirqNXjH.js → filters--kKP-EC8.js} +1 -1
  121. package/core/built/admin/assets/filters-DxA_a4FP.js +1 -0
  122. package/core/built/admin/assets/{first-promoter-modal-CT6MxYZb.js → first-promoter-modal-BYDtrz2S.js} +1 -1
  123. package/core/built/admin/assets/focus-koenig-editor-on-bottom-click-BbSnGOtK.js +1 -0
  124. package/core/built/admin/assets/{ghost-8eabfc1ece81b03cf7b90780d31487e8.css → ghost-8fd5bb799356bbefb254210ba7e1236d.css} +1 -1
  125. package/core/built/admin/assets/{ghost-c2848508292937415f20f5a830eea59e.js → ghost-b4e8a403baba61905cd7560e30b9732d.js} +43 -27
  126. package/core/built/admin/assets/{ghost-dark-a7bebe2c561f55369dcf29f4c820f7ff.css → ghost-dark-cac0cb2d5bb78e9bd14d8d1c2768ed45.css} +1 -1
  127. package/core/built/admin/assets/{gift-link-modal-CQVAye17.js → gift-link-modal-B7u5bD4e.js} +1 -1
  128. package/core/built/admin/assets/{gift-links-DLDS7ljO.js → gift-links-CfLSnst2.js} +1 -1
  129. package/core/built/admin/assets/growth-Cd1uO9d2.js +1 -0
  130. package/core/built/admin/assets/growth-aG32xfdE.js +3 -0
  131. package/core/built/admin/assets/{history-modal-CFd1Hvx6.js → history-modal-Df8lW8h-.js} +1 -1
  132. package/core/built/admin/assets/hooks-Cy_-oZ4o.js +2 -0
  133. package/core/built/admin/assets/{html-field-CJBCLTvv.js → html-field-DA4eU7eq.js} +1 -1
  134. package/core/built/admin/assets/{images-scfkWZiK.js → images-KXmyO3Ss.js} +1 -1
  135. package/core/built/admin/assets/{inbox-Mcp4DXZN.js → inbox-gJHG7_xd.js} +6 -6
  136. package/core/built/admin/assets/index-BHHXtR5C.css +2 -0
  137. package/core/built/admin/assets/index-CgMCwsxN.js +4 -0
  138. package/core/built/admin/assets/index.es-CY6-yuw9.js +1 -0
  139. package/core/built/admin/assets/{input-group-CMhe2DvY.js → input-group-BE8rM2y1.js} +1 -1
  140. package/core/built/admin/assets/{integrations-Bjbu86Z0.js → integrations-B4kuRBFY.js} +1 -1
  141. package/core/built/admin/assets/{invite-user-modal-DfheMXs0.js → invite-user-modal-B4CuNcx4.js} +1 -1
  142. package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +2 -2
  143. package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +2 -2
  144. package/core/built/admin/assets/{koenig-lexical-DBSqAuiI.js → koenig-lexical-CGXb0F8o.js} +2 -2
  145. package/core/built/admin/assets/koenig-loader-BeM5rFPX.js +2 -0
  146. package/core/built/admin/assets/{kpi-card-DBRMfGOW.js → kpi-card-B21aToe8.js} +1 -1
  147. package/core/built/admin/assets/{kpi-card-DFafD1qq.js → kpi-card-pz6Wv4Ae.js} +1 -1
  148. package/core/built/admin/assets/{kpi-helpers-Cv8i8hns.js → kpi-helpers-Dq5jelmP.js} +1 -1
  149. package/core/built/admin/assets/{kpi-tabs-BJItvLYm.js → kpi-tabs-DEZHpKUt.js} +1 -1
  150. package/core/built/admin/assets/{label-BnSCvffu.js → label-OLCzze8N.js} +1 -1
  151. package/core/built/admin/assets/label-picker-BigOvMPV.js +1 -0
  152. package/core/built/admin/assets/{labels-CH3Kn8RV.js → labels-Dr0SgUQD.js} +1 -1
  153. package/core/built/admin/assets/layout-DUTL8SH5.js +1 -0
  154. package/core/built/admin/assets/lib-7QPTI0FS.js +1 -0
  155. package/core/built/admin/assets/link-helpers-BRAg9SCj.js +1 -0
  156. package/core/built/admin/assets/links-BDHPTlyj.js +1 -0
  157. package/core/built/admin/assets/{member-custom-fields-DDQV6QeG.js → member-custom-fields-TRTHCAz9.js} +1 -1
  158. package/core/built/admin/assets/member-detail-b6dRwc3C.js +1 -0
  159. package/core/built/admin/assets/members-Czt5lJSc.js +4 -0
  160. package/core/built/admin/assets/{moderation-AgCIEso4.js → moderation-ZWefD9uH.js} +1 -1
  161. package/core/built/admin/assets/{multi-select-combobox-BcITkUVr.js → multi-select-combobox-CT13MTo-.js} +1 -1
  162. package/core/built/admin/assets/navigation-modal-C8FEhBK5.js +1 -0
  163. package/core/built/admin/assets/newsletter-5VYW4RyJ.js +1 -0
  164. package/core/built/admin/assets/newsletter-detail-modal-ChjC0ZqH.js +1 -0
  165. package/core/built/admin/assets/{newsletter-emails-DKzwafuh.js → newsletter-emails-DZH6I-PY.js} +1 -1
  166. package/core/built/admin/assets/{newsletters-BRBb_uQC.js → newsletters-DOC9KBcX.js} +1 -1
  167. package/core/built/admin/assets/newsletters-Rq1M3GD-.js +1 -0
  168. package/core/built/admin/assets/{note-DyGHtS9a.js → note-DIV-Pojr.js} +1 -1
  169. package/core/built/admin/assets/{notifications-ZgWniKna.js → notifications-s20-H49V.js} +1 -1
  170. package/core/built/admin/assets/{offers-9J1CSiLy.js → offers--e2nGvkW.js} +1 -1
  171. package/core/built/admin/assets/{offers-container-modal-B-E8TIWr.js → offers-container-modal-BmG5M5Tv.js} +1 -1
  172. package/core/built/admin/assets/onboarding-CUuy9unR.js +1 -0
  173. package/core/built/admin/assets/{onboarding-CzIsre-d.js → onboarding-pXyct0Jm.js} +1 -1
  174. package/core/built/admin/assets/{onboarding-route-CaavZulg.js → onboarding-route-CK4pcXoR.js} +1 -1
  175. package/core/built/admin/assets/overview-CaPsciMs.js +1 -0
  176. package/core/built/admin/assets/overview-a8GfcsKX.js +1 -0
  177. package/core/built/admin/assets/{page-header-De0VzKBi.js → page-header-DM_KyoFI.js} +1 -1
  178. package/core/built/admin/assets/{pagemenu-B5KKCpbA.js → pagemenu-vgZVUVKR.js} +1 -1
  179. package/core/built/admin/assets/pages-CjDY2CBI.js +1 -0
  180. package/core/built/admin/assets/pages-route-BzKZYdV5.js +1 -0
  181. package/core/built/admin/assets/{pintura-modal-BZ2J9w3n.js → pintura-modal-DExCF1ii.js} +1 -1
  182. package/core/built/admin/assets/{portal-frame-B6Hr9wHc.js → portal-frame-Cqanidq4.js} +1 -1
  183. package/core/built/admin/assets/portal-modal-oinBgA27.js +1 -0
  184. package/core/built/admin/assets/post-analytics-C5oJ1dWV.js +1 -0
  185. package/core/built/admin/assets/post-analytics-header-OATo3X_r.js +1 -0
  186. package/core/built/admin/assets/post-analytics-provider-tOpIVGT5.js +1 -0
  187. package/core/built/admin/assets/post-helpers-C88JBa4D.js +1 -0
  188. package/core/built/admin/assets/post-share-modal-DvhC1XgV.js +1 -0
  189. package/core/built/admin/assets/post-utils-DKF4mfuo.js +1 -0
  190. package/core/built/admin/assets/posts-list-screen-o4ZjbgS3.js +2 -0
  191. package/core/built/admin/assets/posts-pCPrhQeR.js +1 -0
  192. package/core/built/admin/assets/posts-route-9F--xdIb.js +1 -0
  193. package/core/built/admin/assets/{preferences-C5YJLp74.js → preferences--vNubsqD.js} +2 -2
  194. package/core/built/admin/assets/{preview-modal-BfeFqi5y.js → preview-modal-YYbzlYdO.js} +1 -1
  195. package/core/built/admin/assets/profile-BllKV3dR.js +1 -0
  196. package/core/built/admin/assets/radio-group-DL8GI8rt.js +1 -0
  197. package/core/built/admin/assets/{referrers-CnhCMWCs.js → referrers-D8oHPmDB.js} +1 -1
  198. package/core/built/admin/assets/router-provider-BSvTXgZY.js +1 -0
  199. package/core/built/admin/assets/schemas-BoYdB6YU.js +64 -0
  200. package/core/built/admin/assets/{select-cb0t_TAJ.js → select-v5YbA3Mw.js} +1 -1
  201. package/core/built/admin/assets/settings-PEsZupw1.js +4 -0
  202. package/core/built/admin/assets/{settings-breadcrumbs-DLzbRRSt.js → settings-breadcrumbs-NwPnCTC7.js} +1 -1
  203. package/core/built/admin/assets/{settings-DZqxR0Da.js → settings-u_s1CRkr.js} +1 -1
  204. package/core/built/admin/assets/{share-modal-D2-Bz1_I.js → share-modal-SskCmVGL.js} +1 -1
  205. package/core/built/admin/assets/{slack-modal-DYDTUria.js → slack-modal-XxSJiYgo.js} +1 -1
  206. package/core/built/admin/assets/{sort-button-VVeQjjbu.js → sort-button-Cx1KtINE.js} +1 -1
  207. package/core/built/admin/assets/{source-icon-Ck5_5Kgq.js → source-icon-Bdp9Im_5.js} +1 -1
  208. package/core/built/admin/assets/stats-D9PDLS3x.js +1 -0
  209. package/core/built/admin/assets/stats-view-W2g6Jsd-.js +1 -0
  210. package/core/built/admin/assets/{step-1-CsaBjeN4.js → step-1-BQYzvBzr.js} +1 -1
  211. package/core/built/admin/assets/{step-2-D2vnxOOM.js → step-2-xES32xJV.js} +1 -1
  212. package/core/built/admin/assets/{step-3-wBoLdGEb.js → step-3-DA_XK5kW.js} +2 -2
  213. package/core/built/admin/assets/{stripe-button-CDgLz0S0.js → stripe-button-CVsLHelt.js} +1 -1
  214. package/core/built/admin/assets/{stripe-connect-modal-CSjMJetP.js → stripe-connect-modal-ClmLOMGS.js} +1 -1
  215. package/core/built/admin/assets/style-Wd9nQubI.css +1 -0
  216. package/core/built/admin/assets/{table-Do6iQ83Q.js → table-BenqVupI.js} +1 -1
  217. package/core/built/admin/assets/{tabs-D2yWLMp_.js → tabs-eBBlxPp8.js} +1 -1
  218. package/core/built/admin/assets/tag-detail-Ce8Se2qd.js +2 -0
  219. package/core/built/admin/assets/tag-picker-C3UR-uJ-.js +1 -0
  220. package/core/built/admin/assets/tags-B-kJxCs7.js +1 -0
  221. package/core/built/admin/assets/{tags-zTFiKRpb.js → tags-ChfdwJyS.js} +1 -1
  222. package/core/built/admin/assets/{testimonials-modal-CtnZBJKl.js → testimonials-modal-9Q6fT06V.js} +1 -1
  223. package/core/built/admin/assets/tier-detail-modal-CDyYdqK7.js +1 -0
  224. package/core/built/admin/assets/tiers-BCpIOGQr.js +1 -0
  225. package/core/built/admin/assets/{toggle-group-ByUJsxwD.js → toggle-group-CviIkyok.js} +1 -1
  226. package/core/built/admin/assets/{topic-filter-BgC3g5_v.js → topic-filter-I2jCBGxm.js} +1 -1
  227. package/core/built/admin/assets/{transistor-modal-Drr0SNfo.js → transistor-modal-p53u3W8y.js} +1 -1
  228. package/core/built/admin/assets/{trend-badge-6ye93YAW.js → trend-badge-CideVzQd.js} +1 -1
  229. package/core/built/admin/assets/{unsplash-modal-p6AoQoWp.js → unsplash-modal-DY8UcKcE.js} +1 -1
  230. package/core/built/admin/assets/{use-analytics-data-CWCMAESo.js → use-analytics-data-DAIGcr2f.js} +1 -1
  231. package/core/built/admin/assets/{use-check-theme-limit-error-Cw3WQHy8.js → use-check-theme-limit-error-DRUjK4YM.js} +1 -1
  232. package/core/built/admin/assets/use-filter-params-JkZhIS8L.js +1 -0
  233. package/core/built/admin/assets/{use-filterable-api-CzXcpUh8.js → use-filterable-api-BVEVMrhZ.js} +1 -1
  234. package/core/built/admin/assets/{use-growth-stats-DCVG3s9w.js → use-growth-stats-937CUj6j.js} +1 -1
  235. package/core/built/admin/assets/use-host-limits-D0Dv3tKA.js +1 -0
  236. package/core/built/admin/assets/{use-limiter-D2WRRx0H.js → use-limiter-CohWfzvK.js} +1 -1
  237. package/core/built/admin/assets/{use-navigate-with-base-path-BHPA228f.js → use-navigate-with-base-path-BkxcPj5l.js} +1 -1
  238. package/core/built/admin/assets/use-pintura-config-xgytjU35.js +1 -0
  239. package/core/built/admin/assets/{use-pintura-editor-BfN1UwzH.js → use-pintura-editor-CaV1mbfO.js} +1 -1
  240. package/core/built/admin/assets/{use-post-referrers-Czg47m8s.js → use-post-referrers-BCQI7fDg.js} +1 -1
  241. package/core/built/admin/assets/{use-reply-chain-data-ns0caK8k.js → use-reply-chain-data-CYRhoaxY.js} +1 -1
  242. package/core/built/admin/assets/{use-router-history-entry-uldn7zJO.js → use-router-history-entry-NOKcd--l.js} +1 -1
  243. package/core/built/admin/assets/{use-setting-group-DpcuCUDg.js → use-setting-group-bLKMwoBT.js} +1 -1
  244. package/core/built/admin/assets/{use-settings-navigation-CZjGrhsp.js → use-settings-navigation-B-qrriKD.js} +1 -1
  245. package/core/built/admin/assets/{use-simple-pagination-DzJWobPR.js → use-simple-pagination-CNPd6Rf6.js} +1 -1
  246. package/core/built/admin/assets/use-staff-users-pRIMIyiW.js +1 -0
  247. package/core/built/admin/assets/{use-tinybird-query-CSnGXzZh.js → use-tinybird-query-CmdbCS9X.js} +1 -1
  248. package/core/built/admin/assets/{use-tinybird-token-DTOKRR7e.js → use-tinybird-token-KLne235s.js} +1 -1
  249. package/core/built/admin/assets/{use-unsaved-changes-guard-CBzs-KPi.js → use-unsaved-changes-guard-7-ug0JdH.js} +1 -1
  250. package/core/built/admin/assets/use-upgrade-route-Dm_qqKr0.js +1 -0
  251. package/core/built/admin/assets/{use-url-input-DFr8lCbS.js → use-url-input-fvp-TpPx.js} +1 -1
  252. package/core/built/admin/assets/user-detail-modal-D3KlGYv-.js +1 -0
  253. package/core/built/admin/assets/{users-Cg-OeGzC.js → users-DVNWrqg9.js} +1 -1
  254. package/core/built/admin/assets/{vendor-5753232c7d6ffb3c44e1eb0a02133953.js → vendor-2dd263291938d407802568d3357ec69e.js} +19 -17
  255. package/core/built/admin/assets/{virtual-list-BW-padMe.js → virtual-list-CZ3RK2Yf.js} +1 -1
  256. package/core/built/admin/assets/{web-C1v33ehA.js → web-ClEJsCAj.js} +2 -2
  257. package/core/built/admin/assets/web-X9rdnrtq.js +1 -0
  258. package/core/built/admin/assets/{zapier-modal-BBFfI6Ml.js → zapier-modal-DfVhV17j.js} +1 -1
  259. package/core/built/admin/index.html +19 -18
  260. package/core/server/adapters/jobs/InMemoryJobsBackend.js +74 -29
  261. package/core/server/api/endpoints/authentication.js +1 -2
  262. package/core/server/api/endpoints/authors-public.js +15 -5
  263. package/core/server/api/endpoints/emails.js +13 -0
  264. package/core/server/api/endpoints/index.js +8 -0
  265. package/core/server/api/endpoints/member-metafields-members.js +23 -0
  266. package/core/server/api/endpoints/member-metafields.js +33 -20
  267. package/core/server/api/endpoints/members-account.js +42 -0
  268. package/core/server/api/endpoints/newsletters-public.js +10 -1
  269. package/core/server/api/endpoints/pages-public.js +15 -5
  270. package/core/server/api/endpoints/posts-public.js +15 -5
  271. package/core/server/api/endpoints/tags-public.js +22 -3
  272. package/core/server/api/endpoints/tiers-checkout-config.js +2 -2
  273. package/core/server/api/endpoints/tiers-public.js +38 -1
  274. package/core/server/api/endpoints/users.js +2 -2
  275. package/core/server/api/endpoints/utils/api-filter-utils.js +170 -3
  276. package/core/server/api/endpoints/utils/public-cache-keys.js +3 -0
  277. package/core/server/api/endpoints/utils/serializers/input/authors.js +2 -0
  278. package/core/server/api/endpoints/utils/serializers/input/members.js +16 -2
  279. package/core/server/api/endpoints/utils/serializers/input/pages.js +2 -0
  280. package/core/server/api/endpoints/utils/serializers/input/posts.js +2 -0
  281. package/core/server/api/endpoints/utils/serializers/input/tags.js +2 -0
  282. package/core/server/api/endpoints/utils/serializers/output/emails.js +7 -0
  283. package/core/server/api/endpoints/utils/serializers/output/index.js +4 -0
  284. package/core/server/api/endpoints/utils/serializers/output/member-metafields.js +3 -3
  285. package/core/server/api/endpoints/utils/serializers/output/members-account.js +19 -0
  286. package/core/server/api/endpoints/utils/serializers/output/members-csv-transform.js +1 -1
  287. package/core/server/api/endpoints/utils/serializers/output/tiers.js +10 -2
  288. package/core/server/api/endpoints/utils/serializers/output/users.js +0 -10
  289. package/core/server/data/exporter/table-lists.js +3 -3
  290. package/core/server/data/migrations/versions/6.53/2026-07-15-20-22-53-add-members-custom-field-values.js +1 -1
  291. package/core/server/data/migrations/versions/6.57/2026-08-04-18-00-00-store-custom-field-values-as-leaf-rows.js +10 -1
  292. package/core/server/data/migrations/versions/6.63/2026-09-03-10-07-03-rename-custom-field-tables-to-metafields.js +169 -0
  293. package/core/server/data/migrations/versions/6.63/2026-09-03-11-03-31-remove-member-custom-field-read-permissions.js +30 -0
  294. package/core/server/data/schema/fixtures/fixtures.json +0 -11
  295. package/core/server/data/schema/lib/default-index-name.js +8 -11
  296. package/core/server/data/schema/schema.js +18 -18
  297. package/core/server/data/seeders/README.md +48 -0
  298. package/core/server/data/tinybird/pipes/mv_hits.pipe +86 -50
  299. package/core/server/lib/db-types/count.js +16 -0
  300. package/core/server/lib/validate-password.js +81 -120
  301. package/core/server/models/action.js +4 -4
  302. package/core/server/models/base/plugins/filtered-collection.js +16 -0
  303. package/core/server/models/member.js +6 -6
  304. package/core/server/models/user.js +1 -1
  305. package/core/server/services/automations/automations-api.js +1 -1
  306. package/core/server/services/automations/database-automations-repository.js +11 -4
  307. package/core/server/services/email-service/CONTEXT.md +37 -0
  308. package/core/server/services/email-service/README.md +42 -0
  309. package/core/server/services/email-service/batch-sending-service.js +2 -0
  310. package/core/server/services/email-service/email-controller.js +18 -2
  311. package/core/server/services/email-service/email-service-wrapper.js +3 -0
  312. package/core/server/services/email-service/sending-status-schema.js +21 -0
  313. package/core/server/services/email-service/sending-status-serializers.js +33 -0
  314. package/core/server/services/email-service/sending-status-service.js +47 -0
  315. package/core/server/services/email-service/sending-status.js +111 -0
  316. package/core/server/services/email-suppression-list/email-suppression-list.js +0 -33
  317. package/core/server/services/email-suppression-list/mailgun-email-suppression-list.js +6 -7
  318. package/core/server/services/gifts/README.md +2 -2
  319. package/core/server/services/gifts/constants.js +1 -1
  320. package/core/server/services/gifts/gift-service.js +19 -57
  321. package/core/server/services/gifts/index.js +0 -2
  322. package/core/server/services/jobs-service/index.js +8 -0
  323. package/core/server/services/jobs-service/jobs-service.js +52 -3
  324. package/core/server/services/jobs-service/register-job-handlers.js +13 -2
  325. package/core/server/services/members/account-service.js +98 -0
  326. package/core/server/services/members/api.js +4 -4
  327. package/core/server/services/members/import-export/export/exporter.js +20 -20
  328. package/core/server/services/members/import-export/import/completion-email.js +6 -6
  329. package/core/server/services/members/import-export/import/importer.js +13 -13
  330. package/core/server/services/members/import-export/index.js +7 -7
  331. package/core/server/services/members/members-api/members-api.js +26 -6
  332. package/core/server/services/members/members-api/repositories/member-repository.js +4 -4
  333. package/core/server/services/members/members-api/services/member-bread-service.js +45 -39
  334. package/core/server/services/members/members-ssr.js +18 -0
  335. package/core/server/services/members/middleware.js +95 -81
  336. package/core/server/services/members/service.js +70 -42
  337. package/core/server/services/members/utils.js +9 -0
  338. package/core/server/services/members-metafields/access.js +28 -0
  339. package/core/server/services/{members-custom-fields → members-metafields}/actions.js +5 -5
  340. package/core/server/services/members-metafields/bindings-service.js +147 -0
  341. package/core/server/services/{members-custom-fields → members-metafields}/codec.js +4 -4
  342. package/core/server/services/{members-custom-fields → members-metafields}/config.js +1 -1
  343. package/core/server/services/{members-custom-fields → members-metafields}/definitions-service.js +17 -23
  344. package/core/server/services/{members-custom-fields → members-metafields}/filter.js +9 -9
  345. package/core/server/services/{members-custom-fields → members-metafields}/index.js +16 -7
  346. package/core/server/services/{members-custom-fields → members-metafields}/key.js +1 -1
  347. package/core/server/services/{members-custom-fields → members-metafields}/models.js +4 -4
  348. package/core/server/services/members-metafields/namespaces.js +34 -0
  349. package/core/server/services/{members-custom-fields → members-metafields}/queries.js +1 -1
  350. package/core/server/services/{members-custom-fields → members-metafields}/schema.js +16 -12
  351. package/core/server/services/{members-custom-fields → members-metafields}/serializers.js +6 -6
  352. package/core/server/services/{members-custom-fields → members-metafields}/values-service.js +32 -22
  353. package/core/server/services/mentions/mention-sending-service.js +26 -9
  354. package/core/server/services/mentions/send-webmentions-job.js +18 -0
  355. package/core/server/services/mentions/service.js +1 -31
  356. package/core/server/services/stripe/services/webhook/checkout-session-event-service.js +2 -2
  357. package/core/server/services/stripe/stripe-service.js +4 -4
  358. package/core/server/services/tier-checkout-config/codec.js +2 -2
  359. package/core/server/services/tier-checkout-config/index.js +2 -1
  360. package/core/server/services/tier-checkout-config/models.js +2 -2
  361. package/core/server/services/tier-checkout-config/queries.js +8 -8
  362. package/core/server/services/tier-checkout-config/serializers.js +57 -0
  363. package/core/server/services/tier-checkout-config/service.js +1 -1
  364. package/core/server/services/tiers/service.js +3 -3
  365. package/core/server/services/url/router-filter.js +8 -10
  366. package/core/server/services/users.js +1 -19
  367. package/core/server/web/api/endpoints/admin/routes.js +1 -0
  368. package/core/server/web/members/account/index.js +2 -0
  369. package/core/server/web/members/account/routes.js +46 -0
  370. package/core/server/web/members/app.js +4 -18
  371. package/core/shared/config/defaults.json +1 -1
  372. package/core/shared/labs.js +4 -2
  373. package/package.json +22 -22
  374. package/pnpm-lock.yaml +513 -571
  375. package/pnpm-workspace.yaml +12 -12
  376. package/components/tryghost-custom-field-types-0.0.0.tgz +0 -0
  377. package/core/built/admin/assets/activitypub/layout-2-0bw6HN.js +0 -1
  378. package/core/built/admin/assets/automated-emails-D5VVMND9.js +0 -1
  379. package/core/built/admin/assets/automations-CzKt5fb8.js +0 -1
  380. package/core/built/admin/assets/chunk.598.7e056fabb47ce7dbd73f.js +0 -366
  381. package/core/built/admin/assets/comments-BLmCmyCs.js +0 -1
  382. package/core/built/admin/assets/custom-field-type-option-BddFlJFa.js +0 -1
  383. package/core/built/admin/assets/design-and-theme-modal-BSmo12Op.js +0 -2
  384. package/core/built/admin/assets/edit-profile-C5Asm4LA.js +0 -1
  385. package/core/built/admin/assets/editor-REokrzci.js +0 -8
  386. package/core/built/admin/assets/filter-sources-BO9xUPj7.js +0 -1
  387. package/core/built/admin/assets/filters-CRUWecfY.js +0 -1
  388. package/core/built/admin/assets/growth-BWIu-6xj.js +0 -3
  389. package/core/built/admin/assets/growth-DU29oxeA.js +0 -1
  390. package/core/built/admin/assets/hooks-CuEZV2-q.js +0 -2
  391. package/core/built/admin/assets/index-6MkPdCtr.js +0 -4
  392. package/core/built/admin/assets/index-Bn7QhcZu.css +0 -2
  393. package/core/built/admin/assets/index.es-CNnuNH0d.js +0 -1
  394. package/core/built/admin/assets/koenig-loader-aFQNlJlD.js +0 -2
  395. package/core/built/admin/assets/label-picker-KVoo91sh.js +0 -1
  396. package/core/built/admin/assets/layout-Bjv2Ll-5.js +0 -1
  397. package/core/built/admin/assets/link-helpers-MVDdnFCY.js +0 -1
  398. package/core/built/admin/assets/member-detail-CdJF6Agw.js +0 -1
  399. package/core/built/admin/assets/members-DjQbq9Z2.js +0 -4
  400. package/core/built/admin/assets/navigation-modal-BkT7b1M2.js +0 -1
  401. package/core/built/admin/assets/newsletter-BMMAuvgq.js +0 -1
  402. package/core/built/admin/assets/newsletter-detail-modal-0SCJ7Zpm.js +0 -1
  403. package/core/built/admin/assets/newsletters-DhNigJUh.js +0 -1
  404. package/core/built/admin/assets/onboarding-qg8Akovm.js +0 -1
  405. package/core/built/admin/assets/overview-BW8GLeYi.js +0 -1
  406. package/core/built/admin/assets/overview-BkXZNzaZ.js +0 -1
  407. package/core/built/admin/assets/pages-jgaFm69d.js +0 -1
  408. package/core/built/admin/assets/pages-route-CIUAQxtY.js +0 -1
  409. package/core/built/admin/assets/portal-modal-D4ceei3t.js +0 -1
  410. package/core/built/admin/assets/post-analytics-header-BYCsT0GP.js +0 -1
  411. package/core/built/admin/assets/post-analytics-ot7f6pih.js +0 -1
  412. package/core/built/admin/assets/post-analytics-provider-Bo1gmYaP.js +0 -1
  413. package/core/built/admin/assets/post-helpers-C5OQMBoi.js +0 -1
  414. package/core/built/admin/assets/post-share-modal-C-PaYZ_e.js +0 -1
  415. package/core/built/admin/assets/posts-DuGazgTb.js +0 -1
  416. package/core/built/admin/assets/posts-list-screen-CBGGgjjj.js +0 -2
  417. package/core/built/admin/assets/posts-route-D6xmS7Vf.js +0 -1
  418. package/core/built/admin/assets/profile-B8mx2yAu.js +0 -1
  419. package/core/built/admin/assets/radio-group-DczlLKOn.js +0 -1
  420. package/core/built/admin/assets/router-provider-B0gZyiGJ.js +0 -1
  421. package/core/built/admin/assets/schemas-DppqmpeJ.js +0 -64
  422. package/core/built/admin/assets/settings-DvfTNdHQ.js +0 -4
  423. package/core/built/admin/assets/stats-Rjmk5owg.js +0 -1
  424. package/core/built/admin/assets/stats-view-BGGmkmgv.js +0 -1
  425. package/core/built/admin/assets/tag-detail-BhXlOsCa.js +0 -2
  426. package/core/built/admin/assets/tags-ldukRApe.js +0 -1
  427. package/core/built/admin/assets/tier-detail-modal-BOs8e_G8.js +0 -1
  428. package/core/built/admin/assets/tiers-Nd_z4BZY.js +0 -1
  429. package/core/built/admin/assets/use-filter-params-BIni9taJ.js +0 -1
  430. package/core/built/admin/assets/use-host-limits-DZtR08oa.js +0 -1
  431. package/core/built/admin/assets/use-pintura-config-h-J0Lvta.js +0 -1
  432. package/core/built/admin/assets/use-staff-users-Csipb-O0.js +0 -1
  433. package/core/built/admin/assets/use-upgrade-route-DhRSo7at.js +0 -1
  434. package/core/built/admin/assets/user-detail-modal-B_iUClI6.js +0 -1
  435. package/core/built/admin/assets/web-C4BKzn2T.js +0 -1
  436. package/core/server/services/email-suppression-list/in-memory-email-suppression-list.js +0 -39
  437. package/core/server/services/members-custom-fields/bindings-service.js +0 -120
  438. /package/core/built/admin/assets/{chunk.492.f24d855d7e3f3aad261c.js.LICENSE.txt → chunk.901.4ccc639821020230bcca.js.LICENSE.txt} +0 -0
  439. /package/core/server/services/{members-custom-fields → members-metafields}/storage.js +0 -0
@@ -2,19 +2,23 @@ TOKEN "axis" READ
2
2
 
3
3
  NODE mv_hits_0
4
4
  SQL >
5
-
6
- SELECT timestamp,
5
+ SELECT
6
+ timestamp,
7
7
  inserted_at,
8
8
  -- payload.meta.received_timestamp may be missing or null
9
- -- Nullable fields incur performance penalty in clickhouse, so we default to zero (unix epoch) instead of null.
10
- parseDateTime64BestEffortOrZero(JSONExtractString(payload, 'meta', 'received_timestamp'), 3) as received_at,
9
+ -- Nullable fields incur performance penalty in clickhouse, so we default to zero (unix epoch)
10
+ -- instead of null.
11
+ parseDateTime64BestEffortOrZero(
12
+ JSONExtractString(payload, 'meta', 'received_timestamp'), 3
13
+ ) as received_at,
11
14
  action,
12
15
  version,
13
16
  coalesce(session_id, '0') as session_id,
14
17
  JSONExtractString(payload, 'locale') as locale,
15
18
  JSONExtractString(payload, 'location') as location,
16
19
  case
17
- when JSONExtractString(payload, 'referrerSource') = '' then JSONExtractString(payload, 'meta', 'referrerSource')
20
+ when JSONExtractString(payload, 'referrerSource') = ''
21
+ then JSONExtractString(payload, 'meta', 'referrerSource')
18
22
  else JSONExtractString(payload, 'referrerSource')
19
23
  end as referrer,
20
24
  JSONExtractString(payload, 'pathname') as pathname,
@@ -35,11 +39,8 @@ SQL >
35
39
  FROM analytics_events
36
40
  where action = 'page_hit'
37
41
 
38
-
39
-
40
42
  NODE mv_hits_1
41
43
  SQL >
42
-
43
44
  SELECT
44
45
  site_uuid,
45
46
  timestamp,
@@ -47,8 +48,10 @@ SQL >
47
48
  inserted_at,
48
49
  case
49
50
  -- set to -1 if received_at is the unix epoch, or if received_at is after inserted_at
50
- when toUnixTimestamp(received_at) = 0 then -1
51
- when received_at > inserted_at then -1
51
+ when toUnixTimestamp(received_at) = 0
52
+ then -1
53
+ when received_at > inserted_at
54
+ then -1
52
55
  else date_diff('millisecond', received_at, inserted_at)
53
56
  end as ingestion_latency_ms,
54
57
  action,
@@ -61,58 +64,91 @@ SQL >
61
64
  gift_link,
62
65
  location,
63
66
  case
64
- when referrer = '' then ''
67
+ when referrer = ''
68
+ then ''
65
69
  -- Social Media Consolidation
66
- when referrer IN ('Facebook', 'www.facebook.com', 'l.facebook.com', 'lm.facebook.com', 'm.facebook.com', 'facebook') then 'Facebook'
67
- when referrer IN ('Twitter', 'x.com', 'com.twitter.android') then 'Twitter'
68
- when referrer IN ('go.bsky.app', 'bsky', 'bsky.app') then 'Bluesky'
69
- when referrer IN ('Instagram', 'www.instagram.com') then 'Instagram'
70
- when referrer IN ('LinkedIn', 'LINKEDIN_COMPANY') then 'LinkedIn'
71
- when referrer IN ('l.threads.com') then 'Threads'
72
-
70
+ when
71
+ referrer IN (
72
+ 'Facebook',
73
+ 'www.facebook.com',
74
+ 'l.facebook.com',
75
+ 'lm.facebook.com',
76
+ 'm.facebook.com',
77
+ 'facebook'
78
+ )
79
+ then 'Facebook'
80
+ when referrer IN ('Twitter', 'x.com', 'com.twitter.android')
81
+ then 'Twitter'
82
+ when referrer IN ('go.bsky.app', 'bsky', 'bsky.app')
83
+ then 'Bluesky'
84
+ when referrer IN ('Instagram', 'www.instagram.com')
85
+ then 'Instagram'
86
+ when referrer IN ('LinkedIn', 'LINKEDIN_COMPANY')
87
+ then 'LinkedIn'
88
+ when referrer IN ('l.threads.com')
89
+ then 'Threads'
73
90
  -- Reddit Ecosystem
74
- when referrer IN ('www.reddit.com', 'out.reddit.com', 'old.reddit.com', 'com.reddit.frontpage') then 'Reddit'
75
-
91
+ when
92
+ referrer
93
+ IN ('www.reddit.com', 'out.reddit.com', 'old.reddit.com', 'com.reddit.frontpage')
94
+ then 'Reddit'
76
95
  -- Search Engines (keep distinctions)
77
- when referrer IN ('search.brave.com') then 'Brave Search'
78
- when referrer IN ('www.ecosia.org') then 'Ecosia'
79
-
96
+ when referrer IN ('search.brave.com')
97
+ then 'Brave Search'
98
+ when referrer IN ('www.ecosia.org')
99
+ then 'Ecosia'
80
100
  -- Email Services
81
- when referrer IN ('Gmail', 'com.google.android.gm', 'mail.google.com') then 'Gmail'
82
- when referrer IN ('Outlook.com') then 'Outlook'
83
- when referrer IN ('Yahoo!', 'www.yahoo.com', 'Yahoo! Mail', 'r.search.yahoo.com') then 'Yahoo!'
84
- when referrer IN ('AOL Mail') then 'AOL Mail'
85
-
101
+ when referrer IN ('Gmail', 'com.google.android.gm', 'mail.google.com')
102
+ then 'Gmail'
103
+ when referrer IN ('Outlook.com')
104
+ then 'Outlook'
105
+ when referrer IN ('Yahoo!', 'www.yahoo.com', 'Yahoo! Mail', 'r.search.yahoo.com')
106
+ then 'Yahoo!'
107
+ when referrer IN ('AOL Mail')
108
+ then 'AOL Mail'
86
109
  -- Content Platforms
87
- when referrer IN ('flipboard', 'flipboard.com', 'flipboard.app') then 'Flipboard'
88
- when referrer IN ('substack', 'substack.com') then 'Substack'
89
- when referrer IN ('Ghost.org', 'ghost.org') then 'Ghost'
90
- when referrer IN ('buffer') then 'Buffer'
91
- when referrer IN ('Taboola') then 'Taboola'
92
- when referrer IN ('AppNexus') then 'AppNexus'
93
-
110
+ when referrer IN ('flipboard', 'flipboard.com', 'flipboard.app')
111
+ then 'Flipboard'
112
+ when referrer IN ('substack', 'substack.com')
113
+ then 'Substack'
114
+ when referrer IN ('Ghost.org', 'ghost.org')
115
+ then 'Ghost'
116
+ when referrer IN ('buffer')
117
+ then 'Buffer'
118
+ when referrer IN ('Taboola')
119
+ then 'Taboola'
120
+ when referrer IN ('AppNexus')
121
+ then 'AppNexus'
94
122
  -- Wikipedia
95
- when referrer IN ('en.wikipedia.org', 'en.m.wikipedia.org') then 'Wikipedia'
96
-
123
+ when referrer IN ('en.wikipedia.org', 'en.m.wikipedia.org')
124
+ then 'Wikipedia'
97
125
  -- Mastodon Network
98
- when referrer IN ('mastodon.social', 'mastodon.online', 'org.joinmastodon.android', 'phanpy.social', 'dev.phanpy.social') then 'Mastodon'
99
-
126
+ when
127
+ referrer IN (
128
+ 'mastodon.social',
129
+ 'mastodon.online',
130
+ 'org.joinmastodon.android',
131
+ 'phanpy.social',
132
+ 'dev.phanpy.social'
133
+ )
134
+ then 'Mastodon'
100
135
  -- News Aggregators
101
- when referrer IN ('www.memeorandum.com', 'memeorandum.com') then 'Memeorandum'
102
- when referrer IN ('ground.news') then 'Ground News'
103
- when referrer IN ('apple.news') then 'Apple News'
104
- when referrer IN ('www.smartnews.com') then 'SmartNews'
105
-
136
+ when referrer IN ('www.memeorandum.com', 'memeorandum.com')
137
+ then 'Memeorandum'
138
+ when referrer IN ('ground.news')
139
+ then 'Ground News'
140
+ when referrer IN ('apple.news')
141
+ then 'Apple News'
142
+ when referrer IN ('www.smartnews.com')
143
+ then 'SmartNews'
106
144
  -- Keep other sources as-is
107
- when domainWithoutWWW(referrer) != '' then domainWithoutWWW(referrer)
145
+ when domainWithoutWWW(referrer) != ''
146
+ then domainWithoutWWW(referrer)
108
147
  else referrer
109
148
  end as source,
110
149
  pathname,
111
150
  href,
112
- case
113
- when device = '' then 'unknown'
114
- else device
115
- end as device,
151
+ case when device = '' then 'unknown' else device end as device,
116
152
  case
117
153
  when match(user_agent, 'windows')
118
154
  then 'windows'
@@ -146,5 +182,5 @@ SQL >
146
182
  utm_content
147
183
  FROM mv_hits_0
148
184
 
149
- TYPE materialized
185
+ TYPE MATERIALIZED
150
186
  DATASOURCE _mv_hits
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DbCount = void 0;
4
+ const zod_1 = require("zod");
5
+ const databaseCountInput = zod_1.z.union([zod_1.z.number(), zod_1.z.string().regex(/^\d+$/)]);
6
+ /**
7
+ * A non-negative integer count, whichever database driver handed it back.
8
+ *
9
+ * Aggregate functions such as COUNT can return a decimal string from MySQL and
10
+ * a number from SQLite. Every read normalises to a safe integer so callers do
11
+ * not need to handle that driver difference themselves.
12
+ */
13
+ exports.DbCount = zod_1.z.codec(databaseCountInput, zod_1.z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER), {
14
+ decode: (stored) => (typeof stored === 'string' ? Number(stored) : stored),
15
+ encode: (value) => value,
16
+ });
@@ -1,131 +1,92 @@
1
- const _ = require('lodash');
2
-
3
- const validator = require('@tryghost/validator');
4
-
5
- const tpl = require('@tryghost/tpl');
6
- const settingsCache = require('../../shared/settings-cache');
7
- const urlUtils = require('../../shared/url-utils').default;
8
-
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.validatePassword = validatePassword;
7
+ const tpl_1 = __importDefault(require("@tryghost/tpl"));
8
+ // @ts-expect-error This module lacks type definitions.
9
+ const validator_1 = __importDefault(require("@tryghost/validator"));
10
+ // @ts-expect-error This module lacks type definitions.
11
+ const settings_cache_1 = __importDefault(require("../../shared/settings-cache"));
12
+ const url_utils_1 = __importDefault(require("../../shared/url-utils"));
9
13
  const messages = {
10
- passwordDoesNotComplyLength: 'Your password must be at least {minLength} characters long.',
11
- passwordDoesNotComplySecurity: 'Sorry, you cannot use an insecure password.',
14
+ passwordTooShort: 'Your password must be at least {minLength} characters long.',
15
+ passwordTooLong: 'Your password is too long.',
16
+ passwordDoesNotComplySecurity: 'Sorry, you cannot use an insecure password.',
12
17
  };
13
-
14
18
  /**
15
19
  * Counts repeated characters in a string. When 50% or more characters are the same,
16
20
  * we return false and therefore invalidate the string.
17
- * @param {string} stringToTest The password string to check.
18
- * @return {boolean}
19
21
  */
20
- function characterOccurance(stringToTest) {
21
- const chars = {};
22
- let allowedOccurancy;
23
- let valid = true;
24
-
25
- stringToTest = _.toString(stringToTest);
26
- allowedOccurancy = stringToTest.length / 2;
27
-
28
- // Loop through string and accumulate character counts
29
- _.each(stringToTest, function (char) {
30
- if (!chars[char]) {
31
- chars[char] = 1;
32
- } else {
33
- chars[char] += 1;
34
- }
35
- });
36
-
37
- // check if any of the accumulated chars exceed the allowed occurancy
38
- // of 50% of the words' length.
39
- _.forIn(chars, function (charCount) {
40
- if (charCount >= allowedOccurancy) {
41
- valid = false;
22
+ function characterOccurrence(stringToTest) {
23
+ const limit = stringToTest.length / 2;
24
+ const counts = new Map();
25
+ for (let i = 0; i < stringToTest.length; i++) {
26
+ const char = stringToTest[i];
27
+ const count = (counts.get(char) || 0) + 1;
28
+ if (count >= limit) {
29
+ return false;
30
+ }
31
+ counts.set(char, count);
42
32
  }
43
- });
44
-
45
- return valid;
33
+ return true;
46
34
  }
47
-
48
- /**
49
- * Validation against simple password rules
50
- * Returns false when validation fails and true for a valid password
51
- * @param {string} password The password string to check.
52
- * @param {string} email The users email address to validate agains password.
53
- * @param {string} [blogTitle] Optional blogTitle value, when blog title is not set yet, e. g. in setup process.
54
- * @return {Object} example for returned validation Object:
55
- * invalid password: `validationResult: {isValid: false, message: 'Sorry, you cannot use an insecure password.'}`
56
- * valid password: `validationResult: {isValid: true}`
57
- */
58
- function validatePassword(password, email, blogTitle) {
59
- const validationResult = { isValid: true };
60
- const disallowedPasswords = ['password', 'ghost', 'passw0rd'];
61
- let blogUrl = urlUtils.urlFor('home', true);
62
-
63
- const badPasswords = [
64
- '1234567890',
65
- 'qwertyuiop',
66
- 'qwertzuiop',
67
- 'asdfghjkl;',
68
- 'abcdefghij',
69
- '0987654321',
70
- '1q2w3e4r5t',
71
- '12345asdfg',
72
- ];
73
-
74
- blogTitle = blogTitle ? blogTitle : settingsCache.get('title');
75
- blogUrl = blogUrl.replace(/^http(s?):\/\//, '');
76
-
77
- // password must be longer than 10 characters
78
- if (!validator.isLength(password, 10)) {
79
- validationResult.isValid = false;
80
- validationResult.message = tpl(messages.passwordDoesNotComplyLength, { minLength: 10 });
81
-
82
- return validationResult;
83
- }
84
-
85
- // dissallow password from badPasswords list (e. g. '1234567890')
86
- _.each(badPasswords, function (badPassword) {
87
- if (badPassword === password) {
88
- validationResult.isValid = false;
35
+ /** Validation against simple password rules. */
36
+ function validatePassword(password, email, siteTitle) {
37
+ // password cannot be longer than 256 code units, for performance
38
+ if (password.length > 256) {
39
+ return { isValid: false, message: (0, tpl_1.default)(messages.passwordTooLong) };
40
+ }
41
+ // password must be longer than 10 characters
42
+ if (!validator_1.default.isLength(password, 10)) {
43
+ return { isValid: false, message: (0, tpl_1.default)(messages.passwordTooShort, { minLength: 10 }) };
44
+ }
45
+ const invalidValidationResult = {
46
+ isValid: false,
47
+ message: (0, tpl_1.default)(messages.passwordDoesNotComplySecurity),
48
+ };
49
+ // password must not match with users' email
50
+ if (email && email.toLowerCase() === password.toLowerCase()) {
51
+ return invalidValidationResult;
52
+ }
53
+ // password must not match with site title
54
+ siteTitle = siteTitle || settings_cache_1.default.get('title');
55
+ if (siteTitle && siteTitle.toLowerCase() === password.toLowerCase()) {
56
+ return invalidValidationResult;
57
+ }
58
+ // password must not match with site URL (without protocol, with or without trailing slash)
59
+ let siteUrl = url_utils_1.default.urlFor('home', true);
60
+ siteUrl = siteUrl.replace(/^http(s?):\/\//, '');
61
+ if (siteUrl &&
62
+ (siteUrl.toLowerCase() === password.toLowerCase() ||
63
+ siteUrl.toLowerCase().replace(/\/$/, '') === password.toLowerCase())) {
64
+ return invalidValidationResult;
65
+ }
66
+ // disallow password from badPasswords list (e. g. '1234567890')
67
+ const badPasswords = [
68
+ '1234567890',
69
+ 'qwertyuiop',
70
+ 'qwertzuiop',
71
+ 'asdfghjkl;',
72
+ 'abcdefghij',
73
+ '0987654321',
74
+ '1q2w3e4r5t',
75
+ '12345asdfg',
76
+ ];
77
+ if (badPasswords.includes(password)) {
78
+ return invalidValidationResult;
79
+ }
80
+ // password must not contain the words 'ghost', 'password', or 'passw0rd'
81
+ const disallowedPasswords = ['password', 'ghost', 'passw0rd'];
82
+ for (const disallowedPassword of disallowedPasswords) {
83
+ if (password.toLowerCase().includes(disallowedPassword)) {
84
+ return invalidValidationResult;
85
+ }
89
86
  }
90
- });
91
-
92
- // password must not match with users' email
93
- if (email && email.toLowerCase() === password.toLowerCase()) {
94
- validationResult.isValid = false;
95
- }
96
-
97
- // password must not contain the words 'ghost', 'password', or 'passw0rd'
98
- _.each(disallowedPasswords, function (disallowedPassword) {
99
- if (password.toLowerCase().indexOf(disallowedPassword) >= 0) {
100
- validationResult.isValid = false;
87
+ // dissallow passwords where 50% or more of characters are the same
88
+ if (!characterOccurrence(password)) {
89
+ return invalidValidationResult;
101
90
  }
102
- });
103
-
104
- // password must not match with blog title
105
- if (blogTitle && blogTitle.toLowerCase() === password.toLowerCase()) {
106
- validationResult.isValid = false;
107
- }
108
-
109
- // password must not match with blog URL (without protocol, with or without trailing slash)
110
- if (
111
- blogUrl &&
112
- (blogUrl.toLowerCase() === password.toLowerCase() ||
113
- blogUrl.toLowerCase().replace(/\/$/, '') === password.toLowerCase())
114
- ) {
115
- validationResult.isValid = false;
116
- }
117
-
118
- // dissallow passwords where 50% or more of characters are the same
119
- if (!characterOccurance(password)) {
120
- validationResult.isValid = false;
121
- }
122
-
123
- // Generic error message for the rules where no dedicated error massage is set
124
- if (!validationResult.isValid && !validationResult.message) {
125
- validationResult.message = tpl(messages.passwordDoesNotComplySecurity);
126
- }
127
-
128
- return validationResult;
91
+ return { isValid: true };
129
92
  }
130
-
131
- module.exports = validatePassword;
@@ -1,10 +1,10 @@
1
1
  const ghostBookshelf = require('./base');
2
2
 
3
- // Member custom fields are owned by a raw-knex service rather than the Bookshelf
3
+ // Member metafields are owned by a raw-knex service rather than the Bookshelf
4
4
  // registry. Actions still need a read model for `include=resource` to recognise
5
5
  // their polymorphic resource type and load the current field definition.
6
- const MemberCustomFieldResource = ghostBookshelf.Model.extend({
7
- tableName: 'members_custom_fields',
6
+ const MemberMetafieldResource = ghostBookshelf.Model.extend({
7
+ tableName: 'members_metafields',
8
8
  });
9
9
 
10
10
  const Action = ghostBookshelf.Model.extend(
@@ -26,7 +26,7 @@ const Action = ghostBookshelf.Model.extend(
26
26
  candidates.push([User, 'security_action']);
27
27
  }
28
28
 
29
- candidates.push([MemberCustomFieldResource, 'member_custom_field']);
29
+ candidates.push([MemberMetafieldResource, 'member_custom_field']);
30
30
 
31
31
  return candidates;
32
32
  },
@@ -1,3 +1,6 @@
1
+ const _ = require('lodash');
2
+ const { hasPosts } = require('@tryghost/bookshelf-plugins');
3
+
1
4
  /**
2
5
  * @param {import('bookshelf')} Bookshelf
3
6
  */
@@ -17,6 +20,19 @@ module.exports = function (Bookshelf) {
17
20
  // Apply model-specific search behavior
18
21
  filteredCollection.applySearchQuery(options);
19
22
 
23
+ // Authors are hidden unless they have posts. Apply that constraint before
24
+ // fetchPage clones its count query so pagination cannot reveal hidden staff.
25
+ // Keep this author-specific: tags have separate, established count semantics.
26
+ if (filteredCollection.shouldHavePosts?.joinTable === 'posts_authors') {
27
+ filteredCollection.query(
28
+ hasPosts.addHasPostsWhere(
29
+ _.result(filteredCollection, 'tableName'),
30
+ filteredCollection.shouldHavePosts,
31
+ ),
32
+ );
33
+ filteredCollection.shouldHavePosts = null;
34
+ }
35
+
20
36
  return filteredCollection;
21
37
  },
22
38
 
@@ -5,9 +5,9 @@ const { chainTransformers } = require('@tryghost/mongo-utils');
5
5
  const config = require('../../shared/config');
6
6
  const { MemberCommentingCodec } = require('../services/members/commenting');
7
7
  const {
8
- CUSTOM_FIELDS_RELATION,
9
- createCustomFieldsFilterTransformer,
10
- } = require('../services/members-custom-fields/filter');
8
+ METAFIELDS_RELATION,
9
+ createMetafieldsFilterTransformer,
10
+ } = require('../services/members-metafields/filter');
11
11
 
12
12
  const DEEP_OFFSET_THRESHOLD = 1000;
13
13
 
@@ -195,8 +195,8 @@ const Member = ghostBookshelf.Model.extend(
195
195
  // getFilteredCollectionQuery. Chaining the metafield filter transformer here covers them
196
196
  // all without any call site wiring it.
197
197
  applyDefaultAndCustomFilters(options) {
198
- if (options.filter && options.filter.includes(`${CUSTOM_FIELDS_RELATION.tableNameAs}.`)) {
199
- const transformer = createCustomFieldsFilterTransformer();
198
+ if (options.filter && options.filter.includes(`${METAFIELDS_RELATION.tableNameAs}.`)) {
199
+ const transformer = createMetafieldsFilterTransformer();
200
200
  options.mongoTransformer = options.mongoTransformer
201
201
  ? chainTransformers(options.mongoTransformer, transformer)
202
202
  : transformer;
@@ -206,7 +206,7 @@ const Member = ghostBookshelf.Model.extend(
206
206
 
207
207
  filterRelations() {
208
208
  return {
209
- metafields: CUSTOM_FIELDS_RELATION,
209
+ metafields: METAFIELDS_RELATION,
210
210
  labels: {
211
211
  tableName: 'labels',
212
212
  type: 'manyToMany',
@@ -6,7 +6,7 @@ const limitService = require('../services/limits');
6
6
  const tpl = require('@tryghost/tpl');
7
7
  const errors = require('@tryghost/errors');
8
8
  const security = require('@tryghost/security');
9
- const validatePassword = require('../lib/validate-password');
9
+ const { validatePassword } = require('../lib/validate-password');
10
10
  const permissions = require('../services/permissions');
11
11
  const urlUtils = require('../../shared/url-utils').default;
12
12
  const { setIsRoles } = require('./role-utils');
@@ -78,7 +78,7 @@ const repository = (0, database_automations_repository_1.createDatabaseAutomatio
78
78
  fakeWaitHoursMultiplier: (0, fake_wait_hours_multiplier_1.parseFakeWaitHoursMultiplier)(config.get('automations:fakeWaitHoursMultiplier')),
79
79
  });
80
80
  async function browse() {
81
- return await repository.browse();
81
+ return await repository.browse({ includeStats: true });
82
82
  }
83
83
  async function read(automationId) {
84
84
  const automation = await repository.getById(automationId);
@@ -35,14 +35,16 @@ const messages = {
35
35
  const DEFAULT_EMAIL_DESIGN_SETTING_REFERENCE = constants_1.DEFAULT_EMAIL_DESIGN_SETTING_SLUG;
36
36
  function createDatabaseAutomationsRepository({ knex, fakeWaitHoursMultiplier, }) {
37
37
  return {
38
- async browse() {
38
+ async browse({ includeStats }) {
39
39
  return await knex.transaction(async (trx) => {
40
40
  await ensureDefaultAutomations(trx);
41
- const rows = await loadAutomations(trx);
41
+ const data = includeStats
42
+ ? (await loadAutomationsWithStats(trx)).map((row) => buildAutomationBrowseResult(row))
43
+ : (await loadAutomations(trx)).map((row) => buildAutomationSummary(row));
42
44
  return {
43
- data: rows.map((row) => buildAutomationBrowseResult(row)),
45
+ data,
44
46
  meta: {
45
- pagination: buildPagination(rows.length),
47
+ pagination: buildPagination(data.length),
46
48
  },
47
49
  };
48
50
  });
@@ -671,6 +673,11 @@ async function loadAutomationBySlug(trx, slug) {
671
673
  return row ?? null;
672
674
  }
673
675
  async function loadAutomations(trx) {
676
+ return await trx('automations')
677
+ .select('id', 'slug', 'name', 'status', 'created_at', 'updated_at')
678
+ .orderBy('name');
679
+ }
680
+ async function loadAutomationsWithStats(trx) {
674
681
  const inProgressRuns = trx('automation_run_steps')
675
682
  .distinct('automation_run_id')
676
683
  .where('status', 'pending')
@@ -0,0 +1,37 @@
1
+ # Newsletter Email Sending
2
+
3
+ Newsletter Email Sending covers the preparation and submission of newsletter emails to the configured email provider.
4
+
5
+ ## Language
6
+
7
+ **Sending status**:
8
+ The state of a newsletter email's send: preparing, submitting, submitted, or failed. It is separate from any later provider-reported outcome.
9
+ _Avoid_: Delivery status, provider outcome
10
+
11
+ **Sending phase**:
12
+ The part of a send, before it is submitted or fails, that progress and failures are reported against: preparing or submitting.
13
+ _Avoid_: Step, stage
14
+
15
+ **Preparing**:
16
+ The sending phase while none of a newsletter email's recipient batches has started submitting, including before any sending work has begun.
17
+ _Avoid_: Pending, queued
18
+
19
+ **Submitting**:
20
+ The sending phase once at least one recipient batch has started submitting to the configured email provider and the send has not yet been submitted or failed.
21
+ _Avoid_: Sending, delivering
22
+
23
+ **Sending progress**:
24
+ The completed and total recipient counts for the current sending phase.
25
+ _Avoid_: Delivery progress, expected count
26
+
27
+ **Submitted**:
28
+ Every batch of a newsletter email has been accepted by the configured email provider. Submission does not mean that recipients have received the email.
29
+ _Avoid_: Sent, delivered
30
+
31
+ **Failed**:
32
+ Sending stopped with an error and will not continue without a retry.
33
+ _Avoid_: Bounced, rejected
34
+
35
+ **Failed during**:
36
+ The sending phase a failed newsletter email was in when sending stopped.
37
+ _Avoid_: Failure step
@@ -0,0 +1,42 @@
1
+ # Email service
2
+
3
+ Renders newsletter emails, splits them into recipient batches, and submits the
4
+ batches to the configured email provider. Domain terms live in
5
+ [CONTEXT.md](CONTEXT.md).
6
+
7
+ ## Sending status
8
+
9
+ The sending status served by the Admin API's `emails/:id/status` endpoint is
10
+ derived on read. `sending-status.ts` owns the derivation from an email and its
11
+ batches with their recipient counts; `SendingStatusService` reads those rows
12
+ and `sending-status-serializers.ts` shapes the response. Submitted is terminal and the batch aggregation only
13
+ describes a send that is still in progress or has failed, so submitted emails answer with the email's stored
14
+ `email_count` as both completed and total; batch creation reconciles that
15
+ column to the recipient rows it built. That also keeps reads of long-finished
16
+ emails cheap, with no query over the batches or recipients. The endpoint is always available; Admin decides
17
+ whether to poll it while a send is active.
18
+
19
+ The phase is read from the batches: an email is submitting once any batch has
20
+ left `pending`, and preparing otherwise. Batch statuses persist across
21
+ attempts, so a retried email reports submitting with frozen progress while it
22
+ waits for its job, and `failed_during` is the same derivation applied to a
23
+ failed email.
24
+
25
+ The rough ETA measures recent recipient throughput, including work completed by
26
+ concurrent workers. It stays `null` until enough timing samples are available in
27
+ the current phase and attempt, so short sends may finish without showing an ETA.
28
+ Progress counts update independently of the estimate.
29
+
30
+ The ETA is always `null` for failed emails and `0` once no work remains in the
31
+ current phase. Batches that failed during the current attempt are not remaining
32
+ work until the email is retried, so the ETA can reach `0` while completed is
33
+ still below total. Consumers should key completion on the status, never the ETA.
34
+
35
+ Ghost does not record when a sending attempt or phase started, so the ETA uses
36
+ the email's `updated_at` as a proxy for the start of the current attempt: the
37
+ sending job saves the email when it takes its status lock and, when the
38
+ recipient count changed, again after creating batches; a retry or boot resume
39
+ saves it when re-queuing the email. Batches completed before that timestamp
40
+ belong to an earlier attempt and are left out of the rate window. The proxy
41
+ only holds while nothing saves the Email model, or sets `emails.updated_at`
42
+ through a raw update, while batches are being submitted.