ghost 6.61.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 (619) hide show
  1. package/components/ghost-storage-base-3.0.0.tgz +0 -0
  2. package/components/tryghost-adapter-base-cache-0.2.0.tgz +0 -0
  3. package/components/tryghost-adapter-base-jobs-0.0.0.tgz +0 -0
  4. package/components/tryghost-adapter-base-redirects-0.0.0.tgz +0 -0
  5. package/components/tryghost-adapter-base-route-settings-0.0.0.tgz +0 -0
  6. package/components/tryghost-adapter-base-scheduling-0.2.3.tgz +0 -0
  7. package/components/tryghost-adapter-base-sso-0.1.3.tgz +0 -0
  8. package/components/tryghost-admin-api-schema-0.0.0.tgz +0 -0
  9. package/components/tryghost-checkout-0.0.0.tgz +0 -0
  10. package/components/tryghost-i18n-0.0.0.tgz +0 -0
  11. package/components/tryghost-kg-card-factory-5.2.4.tgz +0 -0
  12. package/components/tryghost-kg-clean-basic-html-4.3.4.tgz +0 -0
  13. package/components/tryghost-kg-converters-1.2.5.tgz +0 -0
  14. package/components/tryghost-kg-default-cards-10.3.5.tgz +0 -0
  15. package/components/tryghost-kg-default-nodes-2.2.1.tgz +0 -0
  16. package/components/tryghost-kg-default-transforms-1.3.4.tgz +0 -0
  17. package/components/tryghost-kg-html-to-lexical-1.4.0.tgz +0 -0
  18. package/components/tryghost-kg-lexical-html-renderer-1.5.0.tgz +0 -0
  19. package/components/tryghost-kg-markdown-html-renderer-7.2.4.tgz +0 -0
  20. package/components/tryghost-kg-utils-1.1.4.tgz +0 -0
  21. package/components/tryghost-limit-service-0.0.0.tgz +0 -0
  22. package/components/tryghost-metafield-types-0.0.0.tgz +0 -0
  23. package/components/tryghost-nql-string-0.0.0.tgz +0 -0
  24. package/components/tryghost-parse-email-address-0.0.0.tgz +0 -0
  25. package/content/settings/README.md +1 -1
  26. package/content/themes/casper/.gitignore +28 -0
  27. package/content/themes/casper/assets/built/casper.js +1 -1
  28. package/content/themes/casper/assets/built/screen.css +1 -1
  29. package/content/themes/casper/assets/css/screen.css +2 -1
  30. package/content/themes/casper/assets/js/lightbox.js +63 -46
  31. package/content/themes/casper/package.json +6 -6
  32. package/content/themes/casper/page.hbs +1 -0
  33. package/content/themes/casper/post.hbs +1 -0
  34. package/content/themes/source/.gitignore +28 -0
  35. package/content/themes/source/assets/built/screen.css +1 -1
  36. package/content/themes/source/assets/built/source.js +1 -1
  37. package/content/themes/source/assets/css/screen.css +2 -1
  38. package/content/themes/source/assets/js/lightbox.js +62 -45
  39. package/content/themes/source/assets/js/main.js +2 -2
  40. package/content/themes/source/package.json +4 -4
  41. package/content/themes/source/partials/feature-image.hbs +1 -0
  42. package/content/themes/source/partials/post-card.hbs +1 -1
  43. package/core/boot.js +61 -27
  44. package/core/built/admin/assets/Combination-B0l4MKVh.js +41 -0
  45. package/core/built/admin/assets/InterVariable-DiVDrmQJ.woff2 +0 -0
  46. package/core/built/admin/assets/InterVariable-Italic-FCBEiFp6.woff2 +0 -0
  47. package/core/built/admin/assets/{about-Bve7IJS2.js → about-C1C3s5-g.js} +1 -1
  48. package/core/built/admin/assets/accordion-CxGsDKwa.js +1 -0
  49. package/core/built/admin/assets/account-migration-CbUhW6gk.js +1 -0
  50. package/core/built/admin/assets/activitypub/{account-migration-C-aej4x_.js → account-migration-KA6Jjf9L.js} +16 -16
  51. package/core/built/admin/assets/activitypub/activitypub.js +9 -9
  52. package/core/built/admin/assets/activitypub/{avatar-DLPL3Abs.js → avatar-DVJF5bxP.js} +2 -2
  53. package/core/built/admin/assets/activitypub/{avatar-flipboard-C9iDY-QV.js → avatar-flipboard-csQaNeYK.js} +2 -2
  54. package/core/built/admin/assets/activitypub/{bluesky-sharing-BXkA5mqV.js → bluesky-sharing-C7nTDMvw.js} +37 -37
  55. package/core/built/admin/assets/activitypub/{content-formatters-CHcyMFk4.js → content-formatters-CLcjaBwV.js} +4 -4
  56. package/core/built/admin/assets/activitypub/{copy-DIAOQoCZ.js → copy-DcOh8kXw.js} +2 -2
  57. package/core/built/admin/assets/activitypub/{domain-B3ihkPde.js → domain-fZ-X8i4-.js} +121 -121
  58. package/core/built/admin/assets/activitypub/{edit-profile-DsZM3rTX.js → edit-profile-BqMLQV88.js} +40 -40
  59. package/core/built/admin/assets/activitypub/{explore-BodNrZgR.js → explore-B4yPoP8E.js} +27 -27
  60. package/core/built/admin/assets/activitypub/{feed-PwH1iJ84.js → feed-BRC_mvAe.js} +51 -51
  61. package/core/built/admin/assets/activitypub/{field-D06-kmOJ.js → field-COacNgOt.js} +3 -3
  62. package/core/built/admin/assets/activitypub/{hash-Bm3WxYN4.js → hash-C7u-irQQ.js} +2 -2
  63. package/core/built/admin/assets/activitypub/inbox-BW-yvUoN.js +1284 -0
  64. package/core/built/admin/assets/activitypub/{inbox-d8oJRTf4.js → inbox-CbTTdDQ3.js} +2 -2
  65. package/core/built/admin/assets/activitypub/label-kPMmYI7l.js +16 -0
  66. package/core/built/admin/assets/activitypub/layout-BL4_jtuV.js +1 -0
  67. package/core/built/admin/assets/activitypub/{moderation-D1Yapsod.js → moderation-CuidJzc0.js} +91 -91
  68. package/core/built/admin/assets/activitypub/{note-C5zjqq5C.js → note-CAMRRSqD.js} +24 -24
  69. package/core/built/admin/assets/activitypub/{notifications-B9p8eCHD.js → notifications-BmNlXGgp.js} +51 -51
  70. package/core/built/admin/assets/activitypub/{onboarding-CNelD_uF.js → onboarding-Bqg7fh_L.js} +2 -2
  71. package/core/built/admin/assets/activitypub/{onboarding-mat5Hrl0.js → onboarding-Dmun9jr_.js} +2 -2
  72. package/core/built/admin/assets/activitypub/{preferences-BCqUdC3v.js → preferences--oH19wf9.js} +1519 -1519
  73. package/core/built/admin/assets/activitypub/{profile-BRCWoFem.js → profile-DH_tQx2D.js} +185 -185
  74. package/core/built/admin/assets/activitypub/{reply-DYK5vVqb.js → reply-Cap9PxSH.js} +2 -2
  75. package/core/built/admin/assets/activitypub/{routes-BSXT992i.js → routes-DdyY9uFF.js} +744 -735
  76. package/core/built/admin/assets/activitypub/{separator-BbQqTm-p.js → separator-CAxxu8VV.js} +3 -3
  77. package/core/built/admin/assets/activitypub/settings-B6EEcVkW.js +315 -0
  78. package/core/built/admin/assets/activitypub/{site-DHoMupSB.js → site-BYps63oh.js} +2 -2
  79. package/core/built/admin/assets/activitypub/{step-1-B-YfsVeO.js → step-1-CKjR2KAm.js} +6 -6
  80. package/core/built/admin/assets/activitypub/{step-2-DtPQzlTH.js → step-2-B6PKfOUQ.js} +6 -6
  81. package/core/built/admin/assets/activitypub/{step-3-D6BFMXL_.js → step-3-C-qBbcUo.js} +11 -11
  82. package/core/built/admin/assets/activitypub/{tabs-GpNecvfB.js → tabs-DLhZtVde.js} +306 -306
  83. package/core/built/admin/assets/activitypub/{topic-filter-BPDeQQys.js → topic-filter-DsUOuu0k.js} +3 -3
  84. package/core/built/admin/assets/activitypub/{use-activity-pub-queries-BoVOxXkV.js → use-activity-pub-queries-CX9snsLs.js} +3 -3
  85. package/core/built/admin/assets/activitypub/{use-navigate-with-base-path-Bmk5h4lW.js → use-navigate-with-base-path-DsJTGDSS.js} +988 -959
  86. package/core/built/admin/assets/activitypub/{use-reply-chain-data-wVBX7swM.js → use-reply-chain-data-AJi9zkzm.js} +30 -30
  87. package/core/built/admin/assets/activitypub/{users-CoX6FT_K.js → users-inweyKL5.js} +192 -164
  88. package/core/built/admin/assets/activitypub/{x-DbQZOLiI.js → x-DR8AT3-4.js} +2 -2
  89. package/core/built/admin/assets/add-integration-modal-BT_onDJy.js +1 -0
  90. package/core/built/admin/assets/add-newsletter-modal-Cx1kQpiY.js +1 -0
  91. package/core/built/admin/assets/add-recommendation-modal-B5nvAhvr.js +1 -0
  92. package/core/built/admin/assets/analytics-DjoJWPI7.js +1 -0
  93. package/core/built/admin/assets/{announcement-bar-modal-DM3cYOpo.js → announcement-bar-modal-Dwd0EGFP.js} +1 -1
  94. package/core/built/admin/assets/{api-keys-3-0vJC3Y.js → api-keys-BGPzLHzA.js} +1 -1
  95. package/core/built/admin/assets/{api-keys-BLm2NDBh.js → api-keys-BoeacM-s.js} +1 -1
  96. package/core/built/admin/assets/{audience-9mwU5TW6.js → audience-ceV3XVHq.js} +1 -1
  97. package/core/built/admin/assets/automated-emails-BLesE41L.js +1 -0
  98. package/core/built/admin/assets/{automations-sAcJ1334.js → automations-BN4u3GR8.js} +1 -1
  99. package/core/built/admin/assets/automations-C-ZTUwW3.js +1 -0
  100. package/core/built/admin/assets/bluesky-sharing-vzh-oPXL.js +1 -0
  101. package/core/built/admin/assets/{breadcrumb-DLiqCyFc.js → breadcrumb-CaLk2b7D.js} +1 -1
  102. package/core/built/admin/assets/{button-B2jcu6bk.js → button-Dvcv1QSR.js} +1 -1
  103. package/core/built/admin/assets/calendar-Bl9RLcZX.js +1 -0
  104. package/core/built/admin/assets/{chart-helpers-Fo3bUL3K.js → chart-helpers-FHywU0pj.js} +1 -1
  105. package/core/built/admin/assets/{checkbox-BfKktMph.js → checkbox-BtayFtjg.js} +1 -1
  106. package/core/built/admin/assets/chunk.254.035b85ec456ebbd7336b.js +440 -0
  107. package/core/built/admin/assets/{chunk.524.60ca4692d91c6022042f.js → chunk.524.e6975095fccdf22d90f8.js} +5 -5
  108. package/core/built/admin/assets/{chunk.582.525c7617c2d9e94c940a.js → chunk.582.98b485f9b2802f540897.js} +6 -6
  109. package/core/built/admin/assets/{chunk.492.f24d855d7e3f3aad261c.js → chunk.901.4ccc639821020230bcca.js} +9949 -7921
  110. package/core/built/admin/assets/{code-editor-view-DCQJxsfU.js → code-editor-view-C3ZfoF1X.js} +1 -1
  111. package/core/built/admin/assets/{code-editor-view-CYVu4EW6.js → code-editor-view-CAyRWy1s.js} +1 -1
  112. package/core/built/admin/assets/color-picker-G4kGR7rp.js +3 -0
  113. package/core/built/admin/assets/color-picker-field-DQFVTNnT.js +1 -0
  114. package/core/built/admin/assets/combobox-BnYzgEB4.js +1 -0
  115. package/core/built/admin/assets/command-C_vyTikI.js +1 -0
  116. package/core/built/admin/assets/comments-aBrxfnZf.js +1 -0
  117. package/core/built/admin/assets/{config-DOMQDZGZ.js → config-kSHvwdTG.js} +1 -1
  118. package/core/built/admin/assets/confirmation-context-DwXX_Y5h.js +1 -0
  119. package/core/built/admin/assets/{content-api-modal-CyQEC3fr.js → content-api-modal-D2G0nJP6.js} +1 -1
  120. package/core/built/admin/assets/context-menu-DOVB6yaz.js +1 -0
  121. package/core/built/admin/assets/{copy-field-B3M1s78L.js → copy-field-DCJwh9qf.js} +1 -1
  122. package/core/built/admin/assets/create-ghost-browse-value-source-5C53jJaX.js +1 -0
  123. package/core/built/admin/assets/custom-field-type-option-BAuW0F-E.js +1 -0
  124. package/core/built/admin/assets/custom-integration-modal-AbJGVyKc.js +1 -0
  125. package/core/built/admin/assets/{date-range-select-DD2RKkgB.js → date-range-select-BY3K-ukj.js} +1 -1
  126. package/core/built/admin/assets/design-and-theme-modal-BvEy4_pC.js +2 -0
  127. package/core/built/admin/assets/dirty-confirm-dialog-DVPuGz3U.js +1 -0
  128. package/core/built/admin/assets/{disabled-sources-indicator-Cvy47bE2.js → disabled-sources-indicator-Cy7Zp4c8.js} +1 -1
  129. package/core/built/admin/assets/dist-Dcsslyo72.js +1 -0
  130. package/core/built/admin/assets/domain-Dl61QGuk.js +1 -0
  131. package/core/built/admin/assets/dropzone-Dtz5Rmjt.js +5 -0
  132. package/core/built/admin/assets/edit-profile-DNJSMDwm.js +1 -0
  133. package/core/built/admin/assets/editor-DbAen9zf.js +8 -0
  134. package/core/built/admin/assets/editor-screen-BFWThIai.js +26 -0
  135. package/core/built/admin/assets/email-sending-status-context-CWDOD1FN.js +1 -0
  136. package/core/built/admin/assets/email-sending-status-provider-DCPkDICt.js +1 -0
  137. package/core/built/admin/assets/emails-vFMk4yYl.js +1 -0
  138. package/core/built/admin/assets/{embed-signup-form-modal-DeBMLxlL.js → embed-signup-form-modal-DGqsORxS.js} +1 -1
  139. package/core/built/admin/assets/en-B1jX3WMs.js +1 -0
  140. package/core/built/admin/assets/explore-nnRr1Fja.js +1 -0
  141. package/core/built/admin/assets/feature-flag-overrides-context-DQjdJFAk.js +1 -0
  142. package/core/built/admin/assets/feature-image-placeholder-BrIHmPs8.js +1 -0
  143. package/core/built/admin/assets/feed-BNUcNu-W.js +1 -0
  144. package/core/built/admin/assets/feedback-DJ4Zb9rZ.js +1 -0
  145. package/core/built/admin/assets/{field-Cj8lmDOo.js → field-C93_Mg6j.js} +1 -1
  146. package/core/built/admin/assets/filter-bar-Da1qxybu.js +1 -0
  147. package/core/built/admin/assets/filter-sources-CGJb4C5r.js +1 -0
  148. package/core/built/admin/assets/filters--kKP-EC8.js +1 -0
  149. package/core/built/admin/assets/filters-DxA_a4FP.js +1 -0
  150. package/core/built/admin/assets/first-promoter-modal-BYDtrz2S.js +1 -0
  151. package/core/built/admin/assets/focus-koenig-editor-on-bottom-click-BbSnGOtK.js +1 -0
  152. package/core/built/admin/assets/{ghost-c8807202a07c20030acb719ee0114815.css → ghost-8fd5bb799356bbefb254210ba7e1236d.css} +1 -1
  153. package/core/built/admin/assets/{ghost-3894eedcc6968f658578c0abfe942aab.js → ghost-b4e8a403baba61905cd7560e30b9732d.js} +63 -44
  154. package/core/built/admin/assets/{ghost-dark-fbf1e384aaef3ae315e540f0eb7373a7.css → ghost-dark-cac0cb2d5bb78e9bd14d8d1c2768ed45.css} +1 -1
  155. package/core/built/admin/assets/gift-link-modal-B7u5bD4e.js +1 -0
  156. package/core/built/admin/assets/{gift-links-DLDS7ljO.js → gift-links-CfLSnst2.js} +1 -1
  157. package/core/built/admin/assets/{growth-D6mhcx3q.js → growth-Cd1uO9d2.js} +1 -1
  158. package/core/built/admin/assets/growth-aG32xfdE.js +3 -0
  159. package/core/built/admin/assets/history-modal-Df8lW8h-.js +1 -0
  160. package/core/built/admin/assets/hooks-Cy_-oZ4o.js +2 -0
  161. package/core/built/admin/assets/{html-field-C8qVGGC1.js → html-field-DA4eU7eq.js} +1 -1
  162. package/core/built/admin/assets/{images-h8VZEQ9k.js → images-KXmyO3Ss.js} +1 -1
  163. package/core/built/admin/assets/{inbox-BYd1M06Q.js → inbox-gJHG7_xd.js} +14 -14
  164. package/core/built/admin/assets/index-BHHXtR5C.css +2 -0
  165. package/core/built/admin/assets/index-CgMCwsxN.js +4 -0
  166. package/core/built/admin/assets/index.es-CY6-yuw9.js +1 -0
  167. package/core/built/admin/assets/{input-group-D8sCXRF5.js → input-group-BE8rM2y1.js} +1 -1
  168. package/core/built/admin/assets/{integrations-Bjbu86Z0.js → integrations-B4kuRBFY.js} +1 -1
  169. package/core/built/admin/assets/invite-user-modal-B4CuNcx4.js +1 -0
  170. package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +14 -14
  171. package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +3 -3
  172. package/core/built/admin/assets/koenig-lexical/style.css +1 -1
  173. package/core/built/admin/assets/{koenig-lexical-_-IsVI7E.js → koenig-lexical-CGXb0F8o.js} +3 -3
  174. package/core/built/admin/assets/koenig-loader-BeM5rFPX.js +2 -0
  175. package/core/built/admin/assets/{kpi-card-DZQk_0W8.js → kpi-card-B21aToe8.js} +1 -1
  176. package/core/built/admin/assets/{kpi-card-DW_2y3ug.js → kpi-card-pz6Wv4Ae.js} +1 -1
  177. package/core/built/admin/assets/{kpi-helpers-PueTKuA6.js → kpi-helpers-Dq5jelmP.js} +1 -1
  178. package/core/built/admin/assets/{kpi-tabs-CojDs-Qz.js → kpi-tabs-DEZHpKUt.js} +1 -1
  179. package/core/built/admin/assets/{label-DjbT91GM.js → label-OLCzze8N.js} +1 -1
  180. package/core/built/admin/assets/label-picker-BigOvMPV.js +1 -0
  181. package/core/built/admin/assets/{labels-CH3Kn8RV.js → labels-Dr0SgUQD.js} +1 -1
  182. package/core/built/admin/assets/layout-DUTL8SH5.js +1 -0
  183. package/core/built/admin/assets/lib-7QPTI0FS.js +1 -0
  184. package/core/built/admin/assets/link-helpers-BRAg9SCj.js +1 -0
  185. package/core/built/admin/assets/links-BDHPTlyj.js +1 -0
  186. package/core/built/admin/assets/member-custom-fields-TRTHCAz9.js +1 -0
  187. package/core/built/admin/assets/member-detail-b6dRwc3C.js +1 -0
  188. package/core/built/admin/assets/members-Czt5lJSc.js +4 -0
  189. package/core/built/admin/assets/moderation-ZWefD9uH.js +1 -0
  190. package/core/built/admin/assets/{multi-select-combobox-B6LiRzDO.js → multi-select-combobox-CT13MTo-.js} +1 -1
  191. package/core/built/admin/assets/navigation-modal-C8FEhBK5.js +1 -0
  192. package/core/built/admin/assets/newsletter-5VYW4RyJ.js +1 -0
  193. package/core/built/admin/assets/newsletter-detail-modal-ChjC0ZqH.js +1 -0
  194. package/core/built/admin/assets/{newsletter-emails-CFeLlt4z.js → newsletter-emails-DZH6I-PY.js} +1 -1
  195. package/core/built/admin/assets/newsletters-DOC9KBcX.js +1 -0
  196. package/core/built/admin/assets/newsletters-Rq1M3GD-.js +1 -0
  197. package/core/built/admin/assets/note-DIV-Pojr.js +1 -0
  198. package/core/built/admin/assets/notifications-s20-H49V.js +1 -0
  199. package/core/built/admin/assets/nql-BLpCn7qX.js +21 -0
  200. package/core/built/admin/assets/{offers-9J1CSiLy.js → offers--e2nGvkW.js} +1 -1
  201. package/core/built/admin/assets/offers-container-modal-BmG5M5Tv.js +1 -0
  202. package/core/built/admin/assets/onboarding-CUuy9unR.js +1 -0
  203. package/core/built/admin/assets/{onboarding-B4YhlZ0i.js → onboarding-pXyct0Jm.js} +1 -1
  204. package/core/built/admin/assets/{onboarding-route-Dmc4IPm3.js → onboarding-route-CK4pcXoR.js} +1 -1
  205. package/core/built/admin/assets/overview-CaPsciMs.js +1 -0
  206. package/core/built/admin/assets/overview-a8GfcsKX.js +1 -0
  207. package/core/built/admin/assets/{page-header-BR_nbIPp.js → page-header-DM_KyoFI.js} +1 -1
  208. package/core/built/admin/assets/pagemenu-vgZVUVKR.js +1 -0
  209. package/core/built/admin/assets/pages-CjDY2CBI.js +1 -0
  210. package/core/built/admin/assets/pages-route-BzKZYdV5.js +1 -0
  211. package/core/built/admin/assets/papaparse.min-cQwnxGCG.js +10 -0
  212. package/core/built/admin/assets/{pintura-modal-BR9hEvS8.js → pintura-modal-DExCF1ii.js} +1 -1
  213. package/core/built/admin/assets/portal-frame-Cqanidq4.js +1 -0
  214. package/core/built/admin/assets/portal-modal-oinBgA27.js +1 -0
  215. package/core/built/admin/assets/post-analytics-C5oJ1dWV.js +1 -0
  216. package/core/built/admin/assets/post-analytics-header-OATo3X_r.js +1 -0
  217. package/core/built/admin/assets/post-analytics-provider-tOpIVGT5.js +1 -0
  218. package/core/built/admin/assets/post-helpers-C88JBa4D.js +1 -0
  219. package/core/built/admin/assets/post-share-modal-DvhC1XgV.js +1 -0
  220. package/core/built/admin/assets/post-utils-DKF4mfuo.js +1 -0
  221. package/core/built/admin/assets/posts-list-screen-o4ZjbgS3.js +2 -0
  222. package/core/built/admin/assets/posts-pCPrhQeR.js +1 -0
  223. package/core/built/admin/assets/posts-route-9F--xdIb.js +1 -0
  224. package/core/built/admin/assets/preferences--vNubsqD.js +5 -0
  225. package/core/built/admin/assets/{preview-modal-C-BL-F9k.js → preview-modal-YYbzlYdO.js} +1 -1
  226. package/core/built/admin/assets/profile-BllKV3dR.js +1 -0
  227. package/core/built/admin/assets/{radio-group-Bgz60i81.js → radio-group-DL8GI8rt.js} +1 -1
  228. package/core/built/admin/assets/{referrers-CnhCMWCs.js → referrers-D8oHPmDB.js} +1 -1
  229. package/core/built/admin/assets/router-provider-BSvTXgZY.js +1 -0
  230. package/core/built/admin/assets/schemas-BoYdB6YU.js +64 -0
  231. package/core/built/admin/assets/select-v5YbA3Mw.js +1 -0
  232. package/core/built/admin/assets/settings-PEsZupw1.js +4 -0
  233. package/core/built/admin/assets/{settings-breadcrumbs-CyP_9zTo.js → settings-breadcrumbs-NwPnCTC7.js} +1 -1
  234. package/core/built/admin/assets/settings-u_s1CRkr.js +1 -0
  235. package/core/built/admin/assets/share-modal-SskCmVGL.js +1 -0
  236. package/core/built/admin/assets/slack-modal-XxSJiYgo.js +1 -0
  237. package/core/built/admin/assets/{sort-button-Cc-Z96sA.js → sort-button-Cx1KtINE.js} +1 -1
  238. package/core/built/admin/assets/{source-icon-v6PUnmyM.js → source-icon-Bdp9Im_5.js} +1 -1
  239. package/core/built/admin/assets/stats-D9PDLS3x.js +1 -0
  240. package/core/built/admin/assets/stats-view-W2g6Jsd-.js +1 -0
  241. package/core/built/admin/assets/{step-1-DSRp2vBj.js → step-1-BQYzvBzr.js} +1 -1
  242. package/core/built/admin/assets/{step-2-COqv2g0u.js → step-2-xES32xJV.js} +1 -1
  243. package/core/built/admin/assets/{step-3-6W1NMVQc.js → step-3-DA_XK5kW.js} +2 -2
  244. package/core/built/admin/assets/{stripe-button-D2K42Uty.js → stripe-button-CVsLHelt.js} +1 -1
  245. package/core/built/admin/assets/stripe-connect-modal-ClmLOMGS.js +1 -0
  246. package/core/built/admin/assets/style-Wd9nQubI.css +1 -0
  247. package/core/built/admin/assets/{table-B2pIKjVh.js → table-BenqVupI.js} +1 -1
  248. package/core/built/admin/assets/{tabs-SaW_UT-Q.js → tabs-eBBlxPp8.js} +1 -1
  249. package/core/built/admin/assets/tag-detail-Ce8Se2qd.js +2 -0
  250. package/core/built/admin/assets/tag-picker-C3UR-uJ-.js +1 -0
  251. package/core/built/admin/assets/tags-B-kJxCs7.js +1 -0
  252. package/core/built/admin/assets/{tags-zTFiKRpb.js → tags-ChfdwJyS.js} +1 -1
  253. package/core/built/admin/assets/testimonials-modal-9Q6fT06V.js +1 -0
  254. package/core/built/admin/assets/tier-detail-modal-CDyYdqK7.js +1 -0
  255. package/core/built/admin/assets/tiers-BCpIOGQr.js +1 -0
  256. package/core/built/admin/assets/{toggle-group-EM9WpESu.js → toggle-group-CviIkyok.js} +1 -1
  257. package/core/built/admin/assets/{topic-filter-DY4gNHn2.js → topic-filter-I2jCBGxm.js} +1 -1
  258. package/core/built/admin/assets/{transistor-modal-CpyPG-MH.js → transistor-modal-p53u3W8y.js} +1 -1
  259. package/core/built/admin/assets/{trend-badge-ByMkuAuH.js → trend-badge-CideVzQd.js} +1 -1
  260. package/core/built/admin/assets/{unsplash-modal-Cv2Q6GtY.js → unsplash-modal-DY8UcKcE.js} +1 -1
  261. package/core/built/admin/assets/{use-analytics-data-CHvjMYuT.js → use-analytics-data-DAIGcr2f.js} +1 -1
  262. package/core/built/admin/assets/use-check-theme-limit-error-B8bjF-gO.css +1 -0
  263. package/core/built/admin/assets/{use-check-theme-limit-error-Qv4SVXqw.js → use-check-theme-limit-error-DRUjK4YM.js} +1 -1
  264. package/core/built/admin/assets/use-filter-params-JkZhIS8L.js +1 -0
  265. package/core/built/admin/assets/{use-filterable-api-CzXcpUh8.js → use-filterable-api-BVEVMrhZ.js} +1 -1
  266. package/core/built/admin/assets/use-growth-stats-937CUj6j.js +1 -0
  267. package/core/built/admin/assets/use-host-limits-D0Dv3tKA.js +1 -0
  268. package/core/built/admin/assets/{use-limiter-cwJvsNBo.js → use-limiter-CohWfzvK.js} +1 -1
  269. package/core/built/admin/assets/{use-navigate-with-base-path-BHPA228f.js → use-navigate-with-base-path-BkxcPj5l.js} +1 -1
  270. package/core/built/admin/assets/use-pintura-config-xgytjU35.js +1 -0
  271. package/core/built/admin/assets/{use-pintura-editor-BDBXWUYq.js → use-pintura-editor-CaV1mbfO.js} +1 -1
  272. package/core/built/admin/assets/{use-post-referrers-DbY-mzci.js → use-post-referrers-BCQI7fDg.js} +1 -1
  273. package/core/built/admin/assets/use-reply-chain-data-CYRhoaxY.js +1 -0
  274. package/core/built/admin/assets/{use-router-history-entry-Bg6BhBQQ.js → use-router-history-entry-NOKcd--l.js} +1 -1
  275. package/core/built/admin/assets/{use-setting-group-VM7FQwMY.js → use-setting-group-bLKMwoBT.js} +1 -1
  276. package/core/built/admin/assets/use-settings-navigation-B-qrriKD.js +1 -0
  277. package/core/built/admin/assets/{use-simple-pagination-Dum6Irbs.js → use-simple-pagination-CNPd6Rf6.js} +1 -1
  278. package/core/built/admin/assets/use-staff-users-pRIMIyiW.js +1 -0
  279. package/core/built/admin/assets/use-tinybird-query-CmdbCS9X.js +1 -0
  280. package/core/built/admin/assets/use-tinybird-token-KLne235s.js +1 -0
  281. package/core/built/admin/assets/{use-unsaved-changes-guard-BAy0RANR.js → use-unsaved-changes-guard-7-ug0JdH.js} +1 -1
  282. package/core/built/admin/assets/use-upgrade-route-Dm_qqKr0.js +1 -0
  283. package/core/built/admin/assets/{use-url-input-Df5QzNa7.js → use-url-input-fvp-TpPx.js} +1 -1
  284. package/core/built/admin/assets/user-detail-modal-D3KlGYv-.js +1 -0
  285. package/core/built/admin/assets/users-DVNWrqg9.js +1 -0
  286. package/core/built/admin/assets/{vendor-5753232c7d6ffb3c44e1eb0a02133953.js → vendor-2dd263291938d407802568d3357ec69e.js} +19 -17
  287. package/core/built/admin/assets/{virtual-list-DEx8WgiO.js → virtual-list-CZ3RK2Yf.js} +1 -1
  288. package/core/built/admin/assets/web-ClEJsCAj.js +2 -0
  289. package/core/built/admin/assets/web-X9rdnrtq.js +1 -0
  290. package/core/built/admin/assets/{zapier-modal-Cw3BTBUu.js → zapier-modal-DfVhV17j.js} +1 -1
  291. package/core/built/admin/index.html +23 -27
  292. package/core/frontend/apps/private-blogging/lib/views/private.hbs +44 -43
  293. package/core/frontend/helpers/cancel_link.js +2 -2
  294. package/core/frontend/helpers/pagination.js +2 -2
  295. package/core/frontend/helpers/social_accounts.js +2 -2
  296. package/core/frontend/public/ghost.css +1 -1
  297. package/core/frontend/public/ghost.min.css +1 -1
  298. package/core/frontend/services/sitemap/user-map-generator.js +9 -2
  299. package/core/server/adapters/jobs/InMemoryJobsBackend.js +74 -29
  300. package/core/server/api/endpoints/authentication.js +1 -2
  301. package/core/server/api/endpoints/authors-public.js +15 -5
  302. package/core/server/api/endpoints/emails.js +13 -0
  303. package/core/server/api/endpoints/index.js +10 -2
  304. package/core/server/api/endpoints/mail.js +1 -1
  305. package/core/server/api/endpoints/member-metafields-members.js +23 -0
  306. package/core/server/api/endpoints/member-metafields.js +102 -0
  307. package/core/server/api/endpoints/members-account.js +42 -0
  308. package/core/server/api/endpoints/members.js +4 -4
  309. package/core/server/api/endpoints/newsletters-public.js +10 -1
  310. package/core/server/api/endpoints/pages-public.js +15 -5
  311. package/core/server/api/endpoints/posts-public.js +15 -5
  312. package/core/server/api/endpoints/posts.js +3 -3
  313. package/core/server/api/endpoints/tags-public.js +22 -3
  314. package/core/server/api/endpoints/tiers-checkout-config.js +2 -2
  315. package/core/server/api/endpoints/tiers-public.js +38 -1
  316. package/core/server/api/endpoints/users.js +2 -2
  317. package/core/server/api/endpoints/utils/api-filter-utils.js +170 -3
  318. package/core/server/api/endpoints/utils/public-cache-keys.js +3 -0
  319. package/core/server/api/endpoints/utils/serializers/input/authors.js +2 -0
  320. package/core/server/api/endpoints/utils/serializers/input/members.js +20 -9
  321. package/core/server/api/endpoints/utils/serializers/input/pages.js +2 -0
  322. package/core/server/api/endpoints/utils/serializers/input/posts.js +2 -0
  323. package/core/server/api/endpoints/utils/serializers/input/tags.js +2 -0
  324. package/core/server/api/endpoints/utils/serializers/output/emails.js +7 -0
  325. package/core/server/api/endpoints/utils/serializers/output/index.js +6 -2
  326. package/core/server/api/endpoints/utils/serializers/output/member-metafields.js +18 -0
  327. package/core/server/api/endpoints/utils/serializers/output/members-account.js +19 -0
  328. package/core/server/api/endpoints/utils/serializers/output/members-csv-transform.js +1 -1
  329. package/core/server/api/endpoints/utils/serializers/output/members.js +2 -4
  330. package/core/server/api/endpoints/utils/serializers/output/tiers.js +10 -2
  331. package/core/server/api/endpoints/utils/serializers/output/users.js +0 -10
  332. package/core/server/api/endpoints/utils/validators/input/invitations.js +0 -5
  333. package/core/server/api/endpoints/utils/validators/input/pages.js +1 -1
  334. package/core/server/api/endpoints/utils/validators/input/posts.js +1 -1
  335. package/core/server/data/exporter/table-lists.js +3 -3
  336. package/core/server/data/importer/importers/data/data-importer.js +3 -3
  337. package/core/server/data/migrations/utils/migrations.js +1 -1
  338. package/core/server/data/migrations/versions/6.53/2026-07-15-20-22-53-add-members-custom-field-values.js +1 -1
  339. package/core/server/data/migrations/versions/6.57/2026-08-04-18-00-00-store-custom-field-values-as-leaf-rows.js +10 -1
  340. package/core/server/data/migrations/versions/6.63/2026-09-03-10-07-03-rename-custom-field-tables-to-metafields.js +169 -0
  341. package/core/server/data/migrations/versions/6.63/2026-09-03-11-03-31-remove-member-custom-field-read-permissions.js +30 -0
  342. package/core/server/data/schema/fixtures/fixtures.json +0 -11
  343. package/core/server/data/schema/lib/default-index-name.js +8 -11
  344. package/core/server/data/schema/schema.js +18 -18
  345. package/core/server/data/seeders/README.md +48 -0
  346. package/core/server/data/tinybird/pipes/mv_hits.pipe +86 -50
  347. package/core/server/lib/db-types/count.js +16 -0
  348. package/core/server/lib/image/image-size.js +24 -0
  349. package/core/server/lib/package-json/parse.js +2 -2
  350. package/core/server/lib/validate-password.js +81 -120
  351. package/core/server/models/action.js +4 -4
  352. package/core/server/models/base/plugins/filtered-collection.js +16 -0
  353. package/core/server/models/member.js +10 -21
  354. package/core/server/models/post.js +2 -2
  355. package/core/server/models/relations/authors.js +2 -16
  356. package/core/server/models/user.js +10 -3
  357. package/core/server/services/auth/passwordreset.js +13 -0
  358. package/core/server/services/automations/automations-api.js +1 -1
  359. package/core/server/services/automations/database-automations-repository.js +11 -4
  360. package/core/server/services/comments/comments-controller.js +30 -9
  361. package/core/server/services/comments/comments-service.js +55 -5
  362. package/core/server/services/content-import/csv/formula.js +12 -0
  363. package/core/server/services/content-import/csv/index.js +6 -1
  364. package/core/server/services/content-import/csv/parse.js +27 -6
  365. package/core/server/services/content-import/csv/serialize.js +13 -0
  366. package/core/server/services/content-import/import/completion-email.js +164 -0
  367. package/core/server/services/content-import/import/errors-file.js +36 -0
  368. package/core/server/services/content-import/import/importer.js +160 -53
  369. package/core/server/services/content-import/import/local-media-url.js +63 -0
  370. package/core/server/services/content-import/import/media.js +254 -0
  371. package/core/server/services/content-import/import/post-data.js +0 -1
  372. package/core/server/services/content-import/import/post-link.js +11 -0
  373. package/core/server/services/content-import/import/post-repository.js +25 -3
  374. package/core/server/services/content-import/import/reader.js +9 -2
  375. package/core/server/services/content-import/import/report.js +34 -0
  376. package/core/server/services/content-import/import/row.js +0 -2
  377. package/core/server/services/content-import/import/schema.js +1 -0
  378. package/core/server/services/content-import/import/staged-file.js +29 -0
  379. package/core/server/services/content-import/import/store.js +25 -2
  380. package/core/server/services/content-import/index.js +52 -6
  381. package/core/server/services/content-import/jobs/content-csv-import-job.js +20 -0
  382. package/core/server/services/custom-redirects/redirect-config-parser.js +2 -2
  383. package/core/server/services/custom-redirects/redirects-service.js +1 -1
  384. package/core/server/services/custom-redirects/validation.js +1 -1
  385. package/core/server/services/email-address/email-address-parser.js +30 -38
  386. package/core/server/services/email-address/email-address-service.js +37 -4
  387. package/core/server/services/email-analytics/index.js +2 -7
  388. package/core/server/services/email-service/CONTEXT.md +37 -0
  389. package/core/server/services/email-service/README.md +42 -0
  390. package/core/server/services/email-service/batch-sending-service.js +2 -4
  391. package/core/server/services/email-service/email-controller.js +18 -2
  392. package/core/server/services/email-service/email-renderer.js +14 -4
  393. package/core/server/services/email-service/email-service-wrapper.js +3 -9
  394. package/core/server/services/email-service/mailgun-email-provider.js +20 -9
  395. package/core/server/services/email-service/sending-status-schema.js +21 -0
  396. package/core/server/services/email-service/sending-status-serializers.js +33 -0
  397. package/core/server/services/email-service/sending-status-service.js +47 -0
  398. package/core/server/services/email-service/sending-status.js +111 -0
  399. package/core/server/services/email-suppression-list/email-suppression-list.js +0 -33
  400. package/core/server/services/email-suppression-list/mailgun-email-suppression-list.js +6 -7
  401. package/core/server/services/gifts/README.md +2 -2
  402. package/core/server/services/gifts/constants.js +1 -1
  403. package/core/server/services/gifts/email-templates/gift-delivery.hbs +3 -3
  404. package/core/server/services/gifts/gift-email-service.js +0 -1
  405. package/core/server/services/gifts/gift-service.js +19 -57
  406. package/core/server/services/gifts/index.js +3 -5
  407. package/core/server/services/gifts/jobs/index.js +4 -4
  408. package/core/server/services/indexnow-ping/indexnow-ping-service.js +2 -2
  409. package/core/server/services/invitations/accept.js +4 -2
  410. package/core/server/services/jobs/job-service.js +0 -9
  411. package/core/server/services/jobs-service/index.js +8 -0
  412. package/core/server/services/jobs-service/jobs-service.js +52 -3
  413. package/core/server/services/jobs-service/register-job-handlers.js +32 -11
  414. package/core/server/services/mail/ghost-mailer.js +4 -4
  415. package/core/server/services/member-welcome-emails/service.js +7 -7
  416. package/core/server/services/members/account-service.js +98 -0
  417. package/core/server/services/members/api.js +4 -3
  418. package/core/server/services/members/import-export/csv/parse.js +0 -7
  419. package/core/server/services/members/import-export/export/exporter.js +20 -20
  420. package/core/server/services/members/import-export/import/completion-email.js +8 -11
  421. package/core/server/services/members/import-export/import/importer.js +13 -13
  422. package/core/server/services/members/import-export/import/row.js +0 -3
  423. package/core/server/services/members/import-export/index.js +10 -17
  424. package/core/server/services/members/jobs/clean-expired-comped-job.js +7 -0
  425. package/core/server/services/members/jobs/clean-expired-comped-task.js +145 -0
  426. package/core/server/services/members/jobs/index.js +48 -24
  427. package/core/server/services/members/members-api/controllers/member-controller.js +11 -0
  428. package/core/server/services/members/members-api/controllers/router-controller.js +3 -6
  429. package/core/server/services/members/members-api/members-api.js +26 -8
  430. package/core/server/services/members/members-api/repositories/event-repository.js +0 -4
  431. package/core/server/services/members/members-api/repositories/member-repository.js +4 -4
  432. package/core/server/services/members/members-api/services/member-bread-service.js +62 -84
  433. package/core/server/services/members/members-api/services/payments-service.js +1 -1
  434. package/core/server/services/members/members-ssr.js +18 -0
  435. package/core/server/services/members/middleware.js +95 -81
  436. package/core/server/services/members/service.js +71 -47
  437. package/core/server/services/members/utils.js +9 -0
  438. package/core/server/services/members-metafields/access.js +28 -0
  439. package/core/server/services/{members-custom-fields → members-metafields}/actions.js +5 -5
  440. package/core/server/services/members-metafields/bindings-service.js +147 -0
  441. package/core/server/services/members-metafields/codec.js +14 -0
  442. package/core/server/services/{members-custom-fields → members-metafields}/config.js +1 -1
  443. package/core/server/services/{members-custom-fields → members-metafields}/definitions-service.js +68 -22
  444. package/core/server/services/members-metafields/filter.js +181 -0
  445. package/core/server/services/{members-custom-fields → members-metafields}/index.js +16 -7
  446. package/core/server/services/{members-custom-fields → members-metafields}/key.js +2 -1
  447. package/core/server/services/{members-custom-fields → members-metafields}/models.js +5 -5
  448. package/core/server/services/members-metafields/namespaces.js +34 -0
  449. package/core/server/services/{members-custom-fields → members-metafields}/queries.js +1 -1
  450. package/core/server/services/{members-custom-fields → members-metafields}/schema.js +16 -12
  451. package/core/server/services/members-metafields/serializers.js +22 -0
  452. package/core/server/services/{members-custom-fields → members-metafields}/values-service.js +80 -48
  453. package/core/server/services/mentions/mention-controller.js +23 -21
  454. package/core/server/services/mentions/mention-sending-service.js +26 -9
  455. package/core/server/services/mentions/process-webmention-job.js +16 -0
  456. package/core/server/services/mentions/send-webmentions-job.js +18 -0
  457. package/core/server/services/mentions/service.js +7 -33
  458. package/core/server/services/route-settings/validation-errors.js +1 -1
  459. package/core/server/services/route-settings/yaml-parser.js +1 -1
  460. package/core/server/services/settings/settings-service.js +1 -8
  461. package/core/server/services/settings-helpers/settings-helpers.js +4 -6
  462. package/core/server/services/slack-ping/slack-ping-service.js +1 -1
  463. package/core/server/services/staff/staff-service-emails.js +2 -2
  464. package/core/server/services/stripe/config.js +14 -8
  465. package/core/server/services/stripe/service.js +10 -1
  466. package/core/server/services/stripe/services/checkout/completed-session.js +2 -2
  467. package/core/server/services/stripe/services/checkout/session-options.js +18 -15
  468. package/core/server/services/stripe/services/webhook/checkout-session-event-service.js +28 -7
  469. package/core/server/services/stripe/stripe-api.js +2 -0
  470. package/core/server/services/stripe/stripe-service.js +24 -14
  471. package/core/server/services/stripe/webhook-controller.js +10 -0
  472. package/core/server/services/tier-checkout-config/codec.js +11 -7
  473. package/core/server/services/tier-checkout-config/index.js +2 -1
  474. package/core/server/services/tier-checkout-config/models.js +11 -5
  475. package/core/server/services/tier-checkout-config/queries.js +12 -12
  476. package/core/server/services/tier-checkout-config/serializers.js +73 -10
  477. package/core/server/services/tier-checkout-config/service.js +14 -13
  478. package/core/server/services/tiers/service.js +3 -3
  479. package/core/server/services/update-check/index.js +13 -13
  480. package/core/server/services/update-check/jobs/update-check-job.js +7 -0
  481. package/core/server/services/update-check/update-check-service.js +2 -2
  482. package/core/server/services/url/router-filter.js +8 -10
  483. package/core/server/services/users.js +1 -19
  484. package/core/server/web/admin/controller.js +1 -1
  485. package/core/server/web/api/endpoints/admin/routes.js +20 -16
  486. package/core/server/web/members/account/index.js +2 -0
  487. package/core/server/web/members/account/routes.js +46 -0
  488. package/core/server/web/members/app.js +4 -18
  489. package/core/shared/config/defaults.json +1 -1
  490. package/core/shared/config/env/config.testing-mysql.json +1 -1
  491. package/core/shared/labs.js +7 -2
  492. package/package.json +42 -41
  493. package/pnpm-lock.yaml +1159 -582
  494. package/pnpm-workspace.yaml +18 -15
  495. package/components/tryghost-custom-field-types-0.0.0.tgz +0 -0
  496. package/components/tryghost-kg-default-nodes-2.2.0.tgz +0 -0
  497. package/core/built/admin/assets/Combination-5fYEzgbs.js +0 -41
  498. package/core/built/admin/assets/account-migration-DRhAehfw.js +0 -1
  499. package/core/built/admin/assets/activitypub/inbox-BluD2ucT.js +0 -1284
  500. package/core/built/admin/assets/activitypub/label-FHSh6ZAw.js +0 -16
  501. package/core/built/admin/assets/activitypub/layout-Cq4NG0Xr.js +0 -1
  502. package/core/built/admin/assets/activitypub/settings-DQDkr06X.js +0 -315
  503. package/core/built/admin/assets/add-integration-modal-BC0jExDZ.js +0 -1
  504. package/core/built/admin/assets/add-newsletter-modal-DchTkA7l.js +0 -1
  505. package/core/built/admin/assets/add-recommendation-modal-DMvaJZKp.js +0 -1
  506. package/core/built/admin/assets/alert-dialog-QhXzdHfc.js +0 -1
  507. package/core/built/admin/assets/analytics-No1DDY3A.js +0 -1
  508. package/core/built/admin/assets/automated-emails-te1D5kq-.js +0 -1
  509. package/core/built/admin/assets/automations-DpUNUdcZ.js +0 -1
  510. package/core/built/admin/assets/bluesky-sharing-Cjrv_f62.js +0 -1
  511. package/core/built/admin/assets/chunk.896.435fe0637944d4a69810.js +0 -366
  512. package/core/built/admin/assets/color-picker-BTY_NdEm.js +0 -3
  513. package/core/built/admin/assets/color-picker-field-CPlkzOH0.js +0 -1
  514. package/core/built/admin/assets/combobox-DrMtv_4P.js +0 -1
  515. package/core/built/admin/assets/command-ZdgYJpoI.js +0 -1
  516. package/core/built/admin/assets/comments-kidj60-h.js +0 -1
  517. package/core/built/admin/assets/confirmation-context-BhbmQQEl.js +0 -1
  518. package/core/built/admin/assets/custom-field-type-option-CsGIZk1J.js +0 -10
  519. package/core/built/admin/assets/custom-integration-modal-BY37O_9X.js +0 -1
  520. package/core/built/admin/assets/design-and-theme-modal-DUSKM4uC.js +0 -22
  521. package/core/built/admin/assets/dialog-B44iRwlc.js +0 -1
  522. package/core/built/admin/assets/dirty-confirm-dialog-BxgQgQ6H.js +0 -1
  523. package/core/built/admin/assets/dist-Cy8tfrp_.js +0 -1
  524. package/core/built/admin/assets/domain-Ca6KhoQt.js +0 -1
  525. package/core/built/admin/assets/dropzone-Bb-5zQTk.js +0 -5
  526. package/core/built/admin/assets/edit-profile-BdupLlus.js +0 -1
  527. package/core/built/admin/assets/editor-BP883Lpn.js +0 -8
  528. package/core/built/admin/assets/explore-FB-CimXw.js +0 -1
  529. package/core/built/admin/assets/feed-C1GxsmI2.js +0 -1
  530. package/core/built/admin/assets/filter-sources-D5n5bFA2.js +0 -1
  531. package/core/built/admin/assets/filters-DJEzkMUK.js +0 -1
  532. package/core/built/admin/assets/filters-MZXvdH5D.js +0 -1
  533. package/core/built/admin/assets/first-promoter-modal-CGTGLCr5.js +0 -1
  534. package/core/built/admin/assets/gift-link-modal-B6v0tnRN.js +0 -1
  535. package/core/built/admin/assets/growth-BGAJD1rs.js +0 -3
  536. package/core/built/admin/assets/history-modal-iKwUY3XR.js +0 -1
  537. package/core/built/admin/assets/hooks-CuEZV2-q.js +0 -2
  538. package/core/built/admin/assets/index-DjQe-e5s.js +0 -4
  539. package/core/built/admin/assets/index-Dqf2x-ry.css +0 -1
  540. package/core/built/admin/assets/index.es-VihPwfeH.js +0 -1
  541. package/core/built/admin/assets/input--W-rCTrX.js +0 -1
  542. package/core/built/admin/assets/invite-user-modal-MdImLCaJ.js +0 -1
  543. package/core/built/admin/assets/koenig-loader-DYZpm6El.js +0 -2
  544. package/core/built/admin/assets/label-picker-cB51xl1r.js +0 -1
  545. package/core/built/admin/assets/layout-c_B_1hax.js +0 -1
  546. package/core/built/admin/assets/link-helpers-MVDdnFCY.js +0 -1
  547. package/core/built/admin/assets/loading-indicator-BpFWTVgh.js +0 -1
  548. package/core/built/admin/assets/member-custom-fields-qs0dIkxM.js +0 -1
  549. package/core/built/admin/assets/member-detail-B6uec2a6.js +0 -1
  550. package/core/built/admin/assets/members-BQF3CqhI.js +0 -2
  551. package/core/built/admin/assets/moderation-DY4wF1Q3.js +0 -1
  552. package/core/built/admin/assets/navigation-modal-DX-gNF3X.js +0 -1
  553. package/core/built/admin/assets/newsletter-CyQA7m3m.js +0 -1
  554. package/core/built/admin/assets/newsletter-detail-modal-BDrptuxn.js +0 -1
  555. package/core/built/admin/assets/newsletters-DXuNmC42.js +0 -1
  556. package/core/built/admin/assets/newsletters-DhNigJUh.js +0 -1
  557. package/core/built/admin/assets/note-Cgo2LCJ6.js +0 -1
  558. package/core/built/admin/assets/notifications-BC50AMku.js +0 -1
  559. package/core/built/admin/assets/offers-container-modal-DWycIMzT.js +0 -1
  560. package/core/built/admin/assets/onboarding-DGQuV7Ew.js +0 -1
  561. package/core/built/admin/assets/overview-CVbFtn-k.js +0 -1
  562. package/core/built/admin/assets/overview-YKj7SZaK.js +0 -1
  563. package/core/built/admin/assets/pages-B6vr9F_D.js +0 -1
  564. package/core/built/admin/assets/popover-CCy0h5rq.js +0 -1
  565. package/core/built/admin/assets/portal-frame-dc4XKr0e.js +0 -1
  566. package/core/built/admin/assets/portal-modal-BvvdF_B9.js +0 -1
  567. package/core/built/admin/assets/post-analytics-DX470WRG.js +0 -1
  568. package/core/built/admin/assets/post-analytics-header-CXpJmdDv.js +0 -1
  569. package/core/built/admin/assets/post-analytics-provider-CLfKGWZy.js +0 -1
  570. package/core/built/admin/assets/post-helpers-C5OQMBoi.js +0 -1
  571. package/core/built/admin/assets/post-share-modal-Cl0NXRJr.js +0 -1
  572. package/core/built/admin/assets/posts-KY3aUhVV.js +0 -1
  573. package/core/built/admin/assets/preferences-DO0UUWFp.js +0 -5
  574. package/core/built/admin/assets/profile-CxT-0l4d.js +0 -1
  575. package/core/built/admin/assets/router-provider-B0gZyiGJ.js +0 -1
  576. package/core/built/admin/assets/schemas-4UPic-oD.js +0 -64
  577. package/core/built/admin/assets/select-DuDUFdPq.js +0 -1
  578. package/core/built/admin/assets/settings-BjCii7BM.js +0 -1
  579. package/core/built/admin/assets/settings-b8kNzZSQ.js +0 -4
  580. package/core/built/admin/assets/share-modal-BV70mr9k.js +0 -1
  581. package/core/built/admin/assets/shim-ClF_m5ux.js +0 -1
  582. package/core/built/admin/assets/slack-modal-Be2_KK-z.js +0 -1
  583. package/core/built/admin/assets/stats-C4reGh9O.js +0 -1
  584. package/core/built/admin/assets/stats-view-DP9pQ62k.js +0 -1
  585. package/core/built/admin/assets/stripe-connect-modal-Z-UT_BGc.js +0 -1
  586. package/core/built/admin/assets/tag-detail-Dc0nTOtU.js +0 -2
  587. package/core/built/admin/assets/tags-BLaXBwnb.js +0 -1
  588. package/core/built/admin/assets/testimonials-modal-sFwRnNLh.js +0 -1
  589. package/core/built/admin/assets/tier-detail-modal-BCFpWwUS.js +0 -1
  590. package/core/built/admin/assets/tiers-Nd_z4BZY.js +0 -1
  591. package/core/built/admin/assets/use-filter-params-CAvfDvf1.js +0 -1
  592. package/core/built/admin/assets/use-growth-stats-D6mZXGhK.js +0 -1
  593. package/core/built/admin/assets/use-host-limits-DZtR08oa.js +0 -1
  594. package/core/built/admin/assets/use-pintura-config-CzTfnufz.js +0 -1
  595. package/core/built/admin/assets/use-reply-chain-data-BeQwz8oF.js +0 -1
  596. package/core/built/admin/assets/use-settings-navigation-BbrSUVcf.js +0 -1
  597. package/core/built/admin/assets/use-staff-users-cZg4ugOF.js +0 -1
  598. package/core/built/admin/assets/use-tinybird-query-B1IMyAcv.js +0 -39
  599. package/core/built/admin/assets/use-tinybird-token-Bow0Zwn8.js +0 -1
  600. package/core/built/admin/assets/use-upgrade-route-DhRSo7at.js +0 -1
  601. package/core/built/admin/assets/user-detail-modal-yipSoQfv.js +0 -1
  602. package/core/built/admin/assets/users-5msE1WMs.js +0 -1
  603. package/core/built/admin/assets/web-CKcXAu86.js +0 -1
  604. package/core/built/admin/assets/web-DR4TqM3E.js +0 -2
  605. package/core/server/api/endpoints/member-custom-fields.js +0 -90
  606. package/core/server/api/endpoints/utils/serializers/output/member-custom-fields.js +0 -20
  607. package/core/server/services/email-suppression-list/in-memory-email-suppression-list.js +0 -39
  608. package/core/server/services/jobs/worker-model-event-bridge.js +0 -149
  609. package/core/server/services/members/jobs/clean-expired-comped.js +0 -132
  610. package/core/server/services/members-custom-fields/bindings-service.js +0 -119
  611. package/core/server/services/members-custom-fields/codec.js +0 -15
  612. package/core/server/services/members-custom-fields/filter.js +0 -199
  613. package/core/server/services/members-custom-fields/serializers.js +0 -24
  614. package/core/server/services/stripe/services/checkout/allowed-countries.js +0 -267
  615. package/core/server/services/stripe/services/checkout/field-ports.js +0 -34
  616. package/core/server/services/tier-checkout-config/destinations.js +0 -17
  617. package/core/server/services/update-check/run-update-check.js +0 -63
  618. /package/core/built/admin/assets/{chunk.492.f24d855d7e3f3aad261c.js.LICENSE.txt → chunk.901.4ccc639821020230bcca.js.LICENSE.txt} +0 -0
  619. /package/core/server/services/{members-custom-fields → members-metafields}/storage.js +0 -0
@@ -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.
@@ -33,7 +33,6 @@ class BatchSendingService {
33
33
  #models;
34
34
  #db;
35
35
  #sentry;
36
- #debugStorageFilePath;
37
36
  #getRequiredUrlRelations;
38
37
  #shuttingDown = false;
39
38
  #inFlight = new Set();
@@ -68,7 +67,6 @@ class BatchSendingService {
68
67
  * @param {object} [dependencies.BEFORE_RETRY_CONFIG]
69
68
  * @param {object} [dependencies.AFTER_RETRY_CONFIG]
70
69
  * @param {object} [dependencies.MAILGUN_API_RETRY_CONFIG]
71
- * @param {string} [dependencies.debugStorageFilePath]
72
70
  */
73
71
  constructor({
74
72
  emailRenderer,
@@ -83,7 +81,6 @@ class BatchSendingService {
83
81
  BEFORE_RETRY_CONFIG,
84
82
  AFTER_RETRY_CONFIG,
85
83
  MAILGUN_API_RETRY_CONFIG,
86
- debugStorageFilePath,
87
84
  }) {
88
85
  this.#emailRenderer = emailRenderer;
89
86
  this.#sendingService = sendingService;
@@ -93,7 +90,6 @@ class BatchSendingService {
93
90
  this.#models = models;
94
91
  this.#db = db;
95
92
  this.#sentry = sentry;
96
- this.#debugStorageFilePath = debugStorageFilePath;
97
93
  this.#getRequiredUrlRelations = getRequiredUrlRelations;
98
94
 
99
95
  if (BEFORE_RETRY_CONFIG) {
@@ -217,6 +213,7 @@ class BatchSendingService {
217
213
 
218
214
  // Check if email is 'pending' only + change status to submitting in one transaction.
219
215
  // This allows us to have a lock around the email job that makes sure an email can only have one active job.
216
+ // Also stamps updated_at, which SendingStatusService reads as the attempt start; do not save the Email once batches submit.
220
217
  let email;
221
218
  try {
222
219
  email = await this.retryDb(
@@ -708,6 +705,7 @@ class BatchSendingService {
708
705
  const deliveryTimes = this.calculateDeliveryTimes(email, batches.length);
709
706
 
710
707
  // Loop batches and send them via the EmailProvider
708
+ // SendingStatusService treats a batch that fails in this run as finished work; never re-queue it within the run.
711
709
  let succeededCount = 0;
712
710
  const queue = batches.slice();
713
711
 
@@ -1,6 +1,8 @@
1
1
  const errors = require('@tryghost/errors');
2
2
  const tpl = require('@tryghost/tpl');
3
3
 
4
+ /** @typedef {import('./sending-status-service').SendingStatusService} SendingStatusService */
5
+
4
6
  const messages = {
5
7
  postNotFound: 'Post not found.',
6
8
  noEmailsProvided: 'No emails provided.',
@@ -23,16 +25,18 @@ class EmailController {
23
25
  service;
24
26
  models;
25
27
  getRequiredUrlRelations;
28
+ sendingStatusService;
26
29
 
27
30
  /**
28
31
  *
29
32
  * @param {EmailService} service
30
- * @param {{models: {Post: any, Newsletter: any, Email: any}, getRequiredUrlRelations?: () => string[]}} dependencies
33
+ * @param {{models: {Post: any, Newsletter: any, Email: any}, getRequiredUrlRelations?: () => string[], sendingStatusService: SendingStatusService}} dependencies
31
34
  */
32
- constructor(service, { models, getRequiredUrlRelations = () => [] }) {
35
+ constructor(service, { models, getRequiredUrlRelations = () => [], sendingStatusService }) {
33
36
  this.service = service;
34
37
  this.models = models;
35
38
  this.getRequiredUrlRelations = getRequiredUrlRelations;
39
+ this.sendingStatusService = sendingStatusService;
36
40
  }
37
41
 
38
42
  async _getFrameData(frame) {
@@ -143,6 +147,18 @@ class EmailController {
143
147
 
144
148
  return await this.service.retryEmail(email);
145
149
  }
150
+
151
+ async getSendingStatus(frame) {
152
+ const status = await this.sendingStatusService.statusFor(frame.data.id);
153
+
154
+ if (!status) {
155
+ throw new errors.NotFoundError({
156
+ message: tpl(messages.emailNotFound),
157
+ });
158
+ }
159
+
160
+ return status;
161
+ }
146
162
  }
147
163
 
148
164
  module.exports = EmailController;
@@ -13,7 +13,7 @@ function isUnsplashImage(url) {
13
13
  }
14
14
  const { DateTime } = require('luxon');
15
15
  const htmlToPlaintext = require('@tryghost/html-to-plaintext');
16
- const EmailAddressParser = require('../email-address/email-address-parser');
16
+ const emailAddressParser = require('../email-address/email-address-parser');
17
17
  const { getEmailDesign } = require('../email-rendering/email-design');
18
18
  const { registerHelpers } = require('./helpers/register-helpers');
19
19
  const crypto = require('crypto');
@@ -229,6 +229,8 @@ function cheerioLoad(html) {
229
229
  * @prop {string} id
230
230
  * @prop {RegExp} token
231
231
  * @prop {(member: MemberLike) => string} getValue
232
+ * @prop {boolean} [trusted] - Value is server-generated, so it is inserted into the
233
+ * html body as-is instead of being HTML-escaped. Defaults to false (escaped).
232
234
  */
233
235
 
234
236
  /**
@@ -337,7 +339,7 @@ class EmailRenderer {
337
339
  }
338
340
 
339
341
  #getRawFromAddress(post, newsletter) {
340
- // Pass the raw name through; EmailAddressParser.stringify() is the single
342
+ // Pass the raw name through; emailAddressParser.stringify() is the single
341
343
  // point that escapes it for the RFC5322 quoted-string From header. Escaping
342
344
  // here too would double-escape (e.g. a title containing a double quote).
343
345
  let senderName = this.#settingsCache.get('title') || '';
@@ -394,7 +396,7 @@ class EmailRenderer {
394
396
  { useFallbackAddress },
395
397
  );
396
398
 
397
- return EmailAddressParser.stringify(addresses.from);
399
+ return emailAddressParser.stringify(addresses.from);
398
400
  }
399
401
 
400
402
  /**
@@ -423,7 +425,7 @@ class EmailRenderer {
423
425
  );
424
426
 
425
427
  if (addresses.replyTo) {
426
- return EmailAddressParser.stringify(addresses.replyTo);
428
+ return emailAddressParser.stringify(addresses.replyTo);
427
429
  }
428
430
  return null;
429
431
  }
@@ -920,18 +922,21 @@ class EmailRenderer {
920
922
  getValue: (member) => {
921
923
  return this.createUnsubscribeUrl(member.uuid, { newsletterUuid });
922
924
  },
925
+ trusted: true, // Server-generated URL, must not be HTML-escaped
923
926
  },
924
927
  {
925
928
  id: 'manage_account_url',
926
929
  getValue: () => {
927
930
  return this.createManageAccountUrl();
928
931
  },
932
+ trusted: true, // Server-generated URL, must not be HTML-escaped
929
933
  },
930
934
  {
931
935
  id: 'uuid',
932
936
  getValue: (member) => {
933
937
  return member.uuid;
934
938
  },
939
+ trusted: true, // Server-generated identifier
935
940
  },
936
941
  {
937
942
  id: 'key',
@@ -941,6 +946,7 @@ class EmailRenderer {
941
946
  .update(member.uuid)
942
947
  .digest('hex');
943
948
  },
949
+ trusted: true, // Server-generated hmac
944
950
  },
945
951
  {
946
952
  id: 'first_name',
@@ -1000,6 +1006,7 @@ class EmailRenderer {
1000
1006
  return this.createUnsubscribeUrl(member.uuid, { newsletterUuid });
1001
1007
  },
1002
1008
  required: true, // Used in email headers
1009
+ trusted: true, // Server-generated URL, must not be HTML-escaped
1003
1010
  },
1004
1011
  // Unique ID used for ad images to bypass ESP image proxies
1005
1012
  {
@@ -1007,6 +1014,7 @@ class EmailRenderer {
1007
1014
  getValue: () => {
1008
1015
  return crypto.randomUUID();
1009
1016
  },
1017
+ trusted: true, // Server-generated identifier
1010
1018
  },
1011
1019
  ];
1012
1020
 
@@ -1043,6 +1051,7 @@ class EmailRenderer {
1043
1051
  getValue: fallback
1044
1052
  ? (member) => definition.getValue(member) || fallback
1045
1053
  : definition.getValue,
1054
+ trusted: definition.trusted === true,
1046
1055
  });
1047
1056
  }
1048
1057
  }
@@ -1056,6 +1065,7 @@ class EmailRenderer {
1056
1065
  originalId: definition.id,
1057
1066
  token: new RegExp(`%%\\{${definition.id}\\}%%`, 'g'),
1058
1067
  getValue: definition.getValue,
1068
+ trusted: definition.trusted === true,
1059
1069
  });
1060
1070
  }
1061
1071
  }
@@ -1,5 +1,4 @@
1
1
  const debug = require('@tryghost/debug')('i18n');
2
- const logging = require('@tryghost/logging');
3
2
  const url = require('../../api/endpoints/utils/serializers/output/utils/url');
4
3
  const events = require('../../lib/common/events');
5
4
 
@@ -25,6 +24,7 @@ class EmailServiceWrapper {
25
24
  const EmailRenderer = require('./email-renderer');
26
25
  const SendingService = require('./sending-service');
27
26
  const BatchSendingService = require('./batch-sending-service');
27
+ const { SendingStatusService } = require('./sending-status-service');
28
28
  const EmailSegmenter = require('./email-segmenter');
29
29
  const MailgunEmailProvider = require('./mailgun-email-provider');
30
30
  const { DomainWarmingService } = require('./domain-warming-service');
@@ -55,12 +55,6 @@ class EmailServiceWrapper {
55
55
  const emailAnalyticsJobs = require('../email-analytics/jobs');
56
56
  const { cachedImageSizeFromUrl } = require('../../lib/image');
57
57
 
58
- // capture errors from mailgun client and log them in sentry
59
- const errorHandler = (error) => {
60
- logging.info(`Capturing error for mailgun email provider service`);
61
- sentry.captureException(error);
62
- };
63
-
64
58
  // Mailgun client instance for email provider
65
59
  const mailgunClient = new MailgunClient({
66
60
  config: configService,
@@ -78,7 +72,6 @@ class EmailServiceWrapper {
78
72
  const mailgunEmailProvider = new MailgunEmailProvider({
79
73
  mailgunClient,
80
74
  config: configService,
81
- errorHandler,
82
75
  });
83
76
 
84
77
  const emailRenderer = new EmailRenderer({
@@ -134,8 +127,8 @@ class EmailServiceWrapper {
134
127
  db,
135
128
  sentry,
136
129
  getRequiredUrlRelations,
137
- debugStorageFilePath: configService.getContentPath('data'),
138
130
  });
131
+ const sendingStatusService = new SendingStatusService({ knex: db.knex });
139
132
 
140
133
  if (ghostServer) {
141
134
  // Two phases: stop claiming batches immediately, drain in-flight ones later.
@@ -178,6 +171,7 @@ class EmailServiceWrapper {
178
171
  Email,
179
172
  },
180
173
  getRequiredUrlRelations,
174
+ sendingStatusService,
181
175
  });
182
176
  }
183
177
  }
@@ -1,6 +1,7 @@
1
1
  const logging = require('@tryghost/logging');
2
2
  const errors = require('@tryghost/errors');
3
3
  const debug = require('@tryghost/debug')('email-service:mailgun-provider-service');
4
+ const { escapeExpression } = require('handlebars');
4
5
 
5
6
  /**
6
7
  * @typedef {object} Config
@@ -35,35 +36,41 @@ const debug = require('@tryghost/debug')('email-service:mailgun-provider-service
35
36
  class MailgunEmailProvider {
36
37
  #mailgunClient;
37
38
  #config;
38
- #errorHandler;
39
39
 
40
40
  /**
41
41
  * @param {object} dependencies
42
42
  * @param {import('../lib/mailgun-client')} dependencies.mailgunClient - mailgun client to send emails
43
43
  * @param {Config} dependencies.config
44
- * @param {Function} [dependencies.errorHandler] - custom error handler for logging exceptions
45
44
  */
46
- constructor({ mailgunClient, config, errorHandler }) {
45
+ constructor({ mailgunClient, config }) {
47
46
  this.#mailgunClient = mailgunClient;
48
47
  this.#config = config;
49
- this.#errorHandler = errorHandler;
50
48
  }
51
49
 
52
- #createRecipientData(replacements) {
50
+ #createRecipientData(replacements, htmlEscapedIds) {
53
51
  let recipientData = {};
54
52
 
55
53
  recipientData = replacements.reduce((acc, replacement) => {
56
54
  const { id, value } = replacement;
57
55
  acc[id] = value;
56
+ // Mailgun's recipient-variables substitution is a simple string replace with
57
+ // no HTML awareness, and the same value is used for both the html and plaintext
58
+ // bodies. Member-controlled values get an extra, escaped variant that only the
59
+ // html body points at. Trusted server-generated values (urls, uuids, hmacs) are
60
+ // skipped so their URLs don't end up entity-encoded inside hrefs.
61
+ if (htmlEscapedIds.has(id)) {
62
+ acc[`${id}_html`] = typeof value === 'string' ? escapeExpression(value) : value;
63
+ }
58
64
  return acc;
59
65
  }, {});
60
66
 
61
67
  return recipientData;
62
68
  }
63
69
 
64
- #updateRecipientVariables(data, replacementDefinitions) {
70
+ #updateRecipientVariables(data, replacementDefinitions, isHtml) {
65
71
  for (const def of replacementDefinitions) {
66
- data = data.replace(def.token, `%recipient.${def.id}%`);
72
+ const useHtmlVariant = isHtml && !def.trusted;
73
+ data = data.replace(def.token, `%recipient.${def.id}${useHtmlVariant ? '_html' : ''}%`);
67
74
  }
68
75
  return data;
69
76
  }
@@ -128,8 +135,11 @@ class MailgunEmailProvider {
128
135
  }
129
136
 
130
137
  // create recipient data for Mailgun using replacement definitions
138
+ const htmlEscapedIds = new Set(
139
+ replacementDefinitions.filter((def) => !def.trusted).map((def) => def.id),
140
+ );
131
141
  const recipientData = recipients.reduce((acc, recipient) => {
132
- acc[recipient.email] = this.#createRecipientData(recipient.replacements);
142
+ acc[recipient.email] = this.#createRecipientData(recipient.replacements, htmlEscapedIds);
133
143
  return acc;
134
144
  }, {});
135
145
 
@@ -139,6 +149,7 @@ class MailgunEmailProvider {
139
149
  messageData[key] = this.#updateRecipientVariables(
140
150
  messageData[key],
141
151
  replacementDefinitions,
152
+ key === 'html',
142
153
  );
143
154
  }
144
155
  });
@@ -165,7 +176,7 @@ class MailgunEmailProvider {
165
176
  message: this.#createMailgunErrorMessage(error),
166
177
  errorDetails: JSON.stringify({ error, messageData }),
167
178
  context: `Mailgun Error ${error.status}: ${error.details}`,
168
- help: `https://ghost.org/docs/newsletters/#bulk-email-configuration`,
179
+ help: `https://docs.ghost.org/newsletters/#bulk-email-configuration`,
169
180
  code: 'BULK_EMAIL_SEND_FAILED',
170
181
  });
171
182
  } else {
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DbBatchSendingRow = exports.DbEmailSendingRow = exports.StoredSendingStatus = void 0;
4
+ const zod_1 = require("zod");
5
+ const count_1 = require("../../lib/db-types/count");
6
+ const date_1 = require("../../lib/db-types/date");
7
+ exports.StoredSendingStatus = zod_1.z.enum(['pending', 'submitting', 'submitted', 'failed']);
8
+ // Projections of tables the Bookshelf models own, not full rows, so there is no knex table
9
+ // augmentation here: typing `emails` by these columns alone would mistype every other read.
10
+ exports.DbEmailSendingRow = zod_1.z.object({
11
+ id: zod_1.z.string(),
12
+ status: exports.StoredSendingStatus,
13
+ email_count: count_1.DbCount,
14
+ updated_at: date_1.DbDate.nullable(),
15
+ });
16
+ exports.DbBatchSendingRow = zod_1.z.object({
17
+ status: exports.StoredSendingStatus,
18
+ created_at: date_1.DbDate,
19
+ updated_at: date_1.DbDate,
20
+ recipient_count: count_1.DbCount,
21
+ });
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toEmailStatusesResponse = void 0;
4
+ const zod_1 = require("zod");
5
+ const case_keys_1 = require("../../lib/case-keys");
6
+ const sending_status_1 = require("./sending-status");
7
+ const SendingPhaseResource = zod_1.z.enum(['preparing', 'submitting']);
8
+ const SendingResource = zod_1.z.object({
9
+ status: zod_1.z.enum(['preparing', 'submitting', 'submitted', 'failed']),
10
+ progress: zod_1.z.object({
11
+ completed: zod_1.z.number(),
12
+ total: zod_1.z.number(),
13
+ estimated_seconds_remaining: zod_1.z.number().nullable(),
14
+ }),
15
+ failed_during: SendingPhaseResource.optional(),
16
+ });
17
+ const EmailStatusResource = zod_1.z.object({
18
+ id: zod_1.z.string(),
19
+ sending: SendingResource,
20
+ });
21
+ const EmailStatusesResponse = zod_1.z.object({ email_statuses: zod_1.z.array(EmailStatusResource) });
22
+ exports.toEmailStatusesResponse = sending_status_1.EmailSendingStatus.transform(({ id, sending }) => ({
23
+ email_statuses: [
24
+ {
25
+ id,
26
+ sending: {
27
+ status: sending.status,
28
+ progress: (0, case_keys_1.snakeKeys)(sending.progress),
29
+ ...(sending.status === 'failed' ? { failed_during: sending.failedDuring } : {}),
30
+ },
31
+ },
32
+ ],
33
+ })).pipe(EmailStatusesResponse);
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SendingStatusService = void 0;
4
+ const case_keys_1 = require("../../lib/case-keys");
5
+ const sending_status_schema_1 = require("./sending-status-schema");
6
+ const sending_status_1 = require("./sending-status");
7
+ class SendingStatusService {
8
+ #knex;
9
+ constructor({ knex }) {
10
+ this.#knex = knex;
11
+ }
12
+ async statusFor(emailId) {
13
+ const row = await this.#knex('emails')
14
+ .select('id', 'status', 'email_count', 'updated_at')
15
+ .where('id', emailId)
16
+ .first();
17
+ if (!row) {
18
+ return null;
19
+ }
20
+ const email = sending_status_schema_1.DbEmailSendingRow.parse(row);
21
+ // A submitted email answers from its own count, and batch creation reconciles email_count
22
+ // to the recipient rows it built, so the batch query is skipped rather than run and ignored.
23
+ const batches = email.status === 'submitted' ? [] : await this.#batchesFor(emailId);
24
+ return {
25
+ id: email.id,
26
+ sending: (0, sending_status_1.buildSendingStatus)({
27
+ status: email.status,
28
+ recipientCount: email.email_count,
29
+ // The sending job saves the email when it takes its status lock, so updated_at
30
+ // stands in for the attempt start that Ghost does not record.
31
+ attemptStartedAt: email.updated_at,
32
+ }, batches),
33
+ };
34
+ }
35
+ async #batchesFor(emailId) {
36
+ // Correlated per-batch count stays on the batch_id index; grouping recipients by email_id scans every recipient row.
37
+ const recipientCount = this.#knex('email_recipients as recipient')
38
+ .count('*')
39
+ .whereRaw('recipient.batch_id = batch.id');
40
+ const rows = await this.#knex('email_batches as batch')
41
+ .select('batch.status', 'batch.created_at', 'batch.updated_at')
42
+ .select(recipientCount.as('recipient_count'))
43
+ .where('batch.email_id', emailId);
44
+ return rows.map((batchRow) => (0, case_keys_1.camelKeys)(sending_status_schema_1.DbBatchSendingRow.parse(batchRow)));
45
+ }
46
+ }
47
+ exports.SendingStatusService = SendingStatusService;
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EmailSendingStatus = exports.SendingStatus = exports.SendingProgress = exports.SendingPhase = void 0;
4
+ exports.buildSendingStatus = buildSendingStatus;
5
+ const zod_1 = require("zod");
6
+ const ETA_COMPLETION_WINDOW = 20;
7
+ const ETA_MIN_INTERVALS = 2;
8
+ exports.SendingPhase = zod_1.z.enum(['preparing', 'submitting']);
9
+ exports.SendingProgress = zod_1.z.object({
10
+ completed: zod_1.z.number().int().nonnegative(),
11
+ total: zod_1.z.number().int().nonnegative(),
12
+ estimatedSecondsRemaining: zod_1.z.number().int().nonnegative().nullable(),
13
+ });
14
+ exports.SendingStatus = zod_1.z.discriminatedUnion('status', [
15
+ zod_1.z.object({ status: exports.SendingPhase, progress: exports.SendingProgress }),
16
+ zod_1.z.object({ status: zod_1.z.literal('submitted'), progress: exports.SendingProgress }),
17
+ zod_1.z.object({ status: zod_1.z.literal('failed'), progress: exports.SendingProgress, failedDuring: exports.SendingPhase }),
18
+ ]);
19
+ exports.EmailSendingStatus = zod_1.z.object({
20
+ id: zod_1.z.string(),
21
+ sending: exports.SendingStatus,
22
+ });
23
+ function buildSendingStatus(email, batches) {
24
+ // Submitted is terminal: every batch was accepted, so the email's own recipient count is the
25
+ // answer and the batch aggregation below, which describes a send still in progress, does not apply.
26
+ if (email.status === 'submitted') {
27
+ return {
28
+ status: 'submitted',
29
+ progress: {
30
+ completed: email.recipientCount,
31
+ total: email.recipientCount,
32
+ estimatedSecondsRemaining: 0,
33
+ },
34
+ };
35
+ }
36
+ const phase = batches.some((batch) => batch.status !== 'pending')
37
+ ? 'submitting'
38
+ : 'preparing';
39
+ const attemptStartedAt = email.attemptStartedAt?.getTime() ?? null;
40
+ const preparedCount = sumRecipients(batches);
41
+ const completedBatches = phase === 'preparing' ? batches : batches.filter((batch) => batch.status === 'submitted');
42
+ const completed = sumRecipients(completedBatches);
43
+ const total = phase === 'preparing' ? Math.max(email.recipientCount, preparedCount) : preparedCount;
44
+ if (email.status === 'failed') {
45
+ return {
46
+ status: 'failed',
47
+ progress: { completed, total, estimatedSecondsRemaining: null },
48
+ failedDuring: phase,
49
+ };
50
+ }
51
+ const failedThisAttempt = batches.filter((batch) => failedDuringAttempt(batch, attemptStartedAt));
52
+ const remaining = total - completed - sumRecipients(failedThisAttempt);
53
+ const samples = completedBatches.map((batch) => ({
54
+ recipientCount: batch.recipientCount,
55
+ timestamp: (phase === 'preparing' ? batch.createdAt : batch.updatedAt).getTime(),
56
+ }));
57
+ return {
58
+ status: phase,
59
+ progress: {
60
+ completed,
61
+ total,
62
+ estimatedSecondsRemaining: estimateSecondsRemaining({
63
+ remaining,
64
+ samples,
65
+ attemptStartedAt,
66
+ }),
67
+ },
68
+ };
69
+ }
70
+ function sumRecipients(batches) {
71
+ return batches.reduce((sum, batch) => sum + batch.recipientCount, 0);
72
+ }
73
+ // A batch that fails is only retried together with its email, so within an attempt it is finished work.
74
+ function failedDuringAttempt(batch, attemptStartedAt) {
75
+ return (batch.status === 'failed' &&
76
+ attemptStartedAt !== null &&
77
+ batch.updatedAt.getTime() >= attemptStartedAt);
78
+ }
79
+ function estimateSecondsRemaining({ remaining, samples, attemptStartedAt, }) {
80
+ if (remaining <= 0) {
81
+ return 0;
82
+ }
83
+ const sorted = samples
84
+ .filter((sample) => sample.recipientCount > 0 &&
85
+ (attemptStartedAt === null || sample.timestamp >= attemptStartedAt))
86
+ .sort((a, b) => a.timestamp - b.timestamp);
87
+ // Coalesce completions sharing a timestamp so database timestamp precision does
88
+ // not turn their recipients into zero-duration samples or make row order matter.
89
+ const completions = [];
90
+ for (const sample of sorted) {
91
+ const previous = completions.at(-1);
92
+ if (previous?.timestamp === sample.timestamp) {
93
+ previous.recipientCount += sample.recipientCount;
94
+ }
95
+ else {
96
+ completions.push({ ...sample });
97
+ }
98
+ }
99
+ // Limit usable timestamps, not rows: fast sends may finish many batches per timestamp.
100
+ const window = completions.slice(-ETA_COMPLETION_WINDOW);
101
+ // Three distinct timestamps provide two measured intervals for an initial rate.
102
+ if (window.length < ETA_MIN_INTERVALS + 1) {
103
+ return null;
104
+ }
105
+ // Measure combined throughput across the window, including overlapping workers.
106
+ // The first completion is only the baseline: its recipients were processed
107
+ // before the measured time span.
108
+ const seconds = (window[window.length - 1].timestamp - window[0].timestamp) / 1000;
109
+ const recipients = window.slice(1).reduce((sum, sample) => sum + sample.recipientCount, 0);
110
+ return Math.ceil((remaining * seconds) / recipients);
111
+ }
@@ -54,38 +54,6 @@ class EmailSuppressionData {
54
54
  }
55
55
  }
56
56
 
57
- /**
58
- * @abstract
59
- * @implements {IEmailSuppressionList}
60
- */
61
- class AbstractEmailSuppressionList {
62
- /**
63
- * @param {string} email
64
- * @returns {Promise<boolean>}
65
- */
66
- // eslint-disable-next-line
67
- async removeEmail(email) {
68
- return Promise.reject();
69
- }
70
-
71
- /**
72
- * @param {string} email
73
- * @returns {Promise<EmailSuppressionData>}
74
- */
75
- // eslint-disable-next-line
76
- async getSuppressionData(email) {
77
- return Promise.reject();
78
- }
79
-
80
- /**
81
- * @param {string[]} emails
82
- * @returns {Promise<EmailSuppressionData[]>}
83
- */
84
- async getBulkSuppressionData(emails) {
85
- return Promise.all(emails.map((email) => this.getSuppressionData(email)));
86
- }
87
- }
88
-
89
57
  class EmailSuppressedEvent {
90
58
  /**
91
59
  * @readonly
@@ -120,7 +88,6 @@ class EmailSuppressedEvent {
120
88
  }
121
89
 
122
90
  module.exports = {
123
- AbstractEmailSuppressionList,
124
91
  EmailSuppressionData,
125
92
  EmailSuppressedEvent,
126
93
  };
@@ -1,13 +1,10 @@
1
- const {
2
- AbstractEmailSuppressionList,
3
- EmailSuppressionData,
4
- EmailSuppressedEvent,
5
- } = require('./email-suppression-list');
1
+ const { EmailSuppressionData, EmailSuppressedEvent } = require('./email-suppression-list');
6
2
  const { SpamComplaintEvent } = require('../email-service/events/spam-complaint-event');
7
3
  const { EmailBouncedEvent } = require('../email-service/events/email-bounced-event');
8
4
  const DomainEvents = require('@tryghost/domain-events');
9
5
  const logging = require('@tryghost/logging');
10
6
  const models = require('../../models');
7
+ /** @import {IEmailSuppressionList} from './email-suppression-list' */
11
8
 
12
9
  /**
13
10
  * @typedef {object} IMailgunAPIClient
@@ -16,14 +13,16 @@ const models = require('../../models');
16
13
  * @prop {(email: string) => Promise<any>} removeUnsubscribe
17
14
  */
18
15
 
19
- class MailgunEmailSuppressionList extends AbstractEmailSuppressionList {
16
+ /**
17
+ * @implements {IEmailSuppressionList}
18
+ */
19
+ class MailgunEmailSuppressionList {
20
20
  /**
21
21
  * @param {object} deps
22
22
  * @param {import('bookshelf').Model} deps.Suppression
23
23
  * @param {IMailgunAPIClient} deps.apiClient
24
24
  */
25
25
  constructor(deps) {
26
- super();
27
26
  this.Suppression = deps.Suppression;
28
27
  this.apiClient = deps.apiClient;
29
28
  }