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
@@ -213,6 +213,7 @@ class BatchSendingService {
213
213
 
214
214
  // Check if email is 'pending' only + change status to submitting in one transaction.
215
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.
216
217
  let email;
217
218
  try {
218
219
  email = await this.retryDb(
@@ -704,6 +705,7 @@ class BatchSendingService {
704
705
  const deliveryTimes = this.calculateDeliveryTimes(email, batches.length);
705
706
 
706
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.
707
709
  let succeededCount = 0;
708
710
  const queue = batches.slice();
709
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;
@@ -24,6 +24,7 @@ class EmailServiceWrapper {
24
24
  const EmailRenderer = require('./email-renderer');
25
25
  const SendingService = require('./sending-service');
26
26
  const BatchSendingService = require('./batch-sending-service');
27
+ const { SendingStatusService } = require('./sending-status-service');
27
28
  const EmailSegmenter = require('./email-segmenter');
28
29
  const MailgunEmailProvider = require('./mailgun-email-provider');
29
30
  const { DomainWarmingService } = require('./domain-warming-service');
@@ -127,6 +128,7 @@ class EmailServiceWrapper {
127
128
  sentry,
128
129
  getRequiredUrlRelations,
129
130
  });
131
+ const sendingStatusService = new SendingStatusService({ knex: db.knex });
130
132
 
131
133
  if (ghostServer) {
132
134
  // Two phases: stop claiming batches immediately, drain in-flight ones later.
@@ -169,6 +171,7 @@ class EmailServiceWrapper {
169
171
  Email,
170
172
  },
171
173
  getRequiredUrlRelations,
174
+ sendingStatusService,
172
175
  });
173
176
  }
174
177
  }
@@ -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
  }
@@ -6,8 +6,8 @@ transactions, or Stripe objects.
6
6
 
7
7
  ## Interface
8
8
 
9
- - `startCheckout(input)` owns the `giftSubCustomization` flag, duration
10
- catalogue, Portal plan gate, tier/cadence validation, authoritative price,
9
+ - `startCheckout(input)` owns the duration catalogue, Portal plan gate,
10
+ tier/cadence validation, authoritative price,
11
11
  token, success parameters, metadata, customer, and one-time payment.
12
12
  - `preparePaidContinuation(input)` validates an active gift and returns the
13
13
  stable tier/cadence/trial decision consumed by the normal subscription
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.GIFT_DELIVERY_EMAIL_TAG = exports.GIFT_DELIVERY_STALE_AFTER_MS = exports.GIFT_REMINDER_FLOOR_DAYS = exports.GIFT_REMINDER_LEAD_DAYS = exports.GIFT_SEND_HOUR = exports.GIFT_MAX_SCHEDULE_DAYS = exports.GIFT_EXPIRY_DAYS = void 0;
4
4
  exports.GIFT_EXPIRY_DAYS = 365;
5
5
  // How far ahead a delivery may be scheduled. Portal's picker carries its own
6
- // copy (GIFT_MAX_SCHEDULE_DAYS in beta-gift-page.tsx) since Portal ships
6
+ // copy (GIFT_MAX_SCHEDULE_DAYS in gift-page.tsx) since Portal ships
7
7
  // separately — change them together.
8
8
  exports.GIFT_MAX_SCHEDULE_DAYS = 365;
9
9
  // Site-local hour a scheduled delivery is sent at. Portal never derives
@@ -95,40 +95,22 @@ class GiftService {
95
95
  this.deps = deps;
96
96
  }
97
97
  async startCheckout(input) {
98
- const customizationEnabled = this.deps.labsService.isSet('giftSubCustomization');
99
- const populatedDeliveryFields = [
100
- input.recipientEmail,
101
- input.recipientName,
102
- input.buyerName,
103
- input.personalMessage,
104
- input.deliveryDate,
105
- ].some((value) => value !== undefined && value !== null && value !== '');
106
- if (!customizationEnabled && (input.deliveryMethod === 'email' || populatedDeliveryFields)) {
107
- throw new errors_1.default.BadRequestError({
108
- message: 'Bad Request.',
109
- context: 'Gift email delivery is not available',
110
- });
111
- }
112
98
  const parsedBuyerEmail = CheckoutBuyerEmailSchema.safeParse(input.buyer.email);
113
- if (customizationEnabled && !parsedBuyerEmail.success) {
99
+ if (!parsedBuyerEmail.success) {
114
100
  throw new errors_1.default.BadRequestError({
115
101
  message: 'Bad Request.',
116
102
  context: `Invalid gift buyer email: ${parsedBuyerEmail.error.issues[0].message}`,
117
103
  });
118
104
  }
119
105
  const buyerEmail = parsedBuyerEmail.success ? parsedBuyerEmail.data : input.buyer.email;
120
- const parsedDelivery = GiftCheckoutDeliverySchema.safeParse(customizationEnabled
121
- ? {
122
- deliveryMethod: input.deliveryMethod ?? 'link',
123
- recipientEmail: input.recipientEmail,
124
- recipientName: input.recipientName,
125
- buyerName: input.buyerName,
126
- personalMessage: input.personalMessage,
127
- deliveryDate: input.deliveryDate,
128
- }
129
- : {
130
- deliveryMethod: 'link',
131
- });
106
+ const parsedDelivery = GiftCheckoutDeliverySchema.safeParse({
107
+ deliveryMethod: input.deliveryMethod ?? 'link',
108
+ recipientEmail: input.recipientEmail,
109
+ recipientName: input.recipientName,
110
+ buyerName: input.buyerName,
111
+ personalMessage: input.personalMessage,
112
+ deliveryDate: input.deliveryDate,
113
+ });
132
114
  if (!parsedDelivery.success) {
133
115
  const issue = parsedDelivery.error.issues[0];
134
116
  throw new errors_1.default.BadRequestError({
@@ -150,22 +132,7 @@ class GiftService {
150
132
  context: 'Expected offerId or tierId, received none',
151
133
  });
152
134
  }
153
- let resolvedDuration = null;
154
- let cadence;
155
- if (customizationEnabled) {
156
- resolvedDuration = (0, gift_checkout_offer_1.resolveGiftDuration)(input);
157
- cadence = resolvedDuration.cadence;
158
- }
159
- else {
160
- if (input.cadence !== 'month' && input.cadence !== 'year') {
161
- const receivedCadence = input.cadence ? `"${input.cadence}"` : input.cadence;
162
- throw new errors_1.default.BadRequestError({
163
- message: 'Bad Request.',
164
- context: `Expected cadence to be "month" or "year", received ${receivedCadence}`,
165
- });
166
- }
167
- cadence = input.cadence;
168
- }
135
+ const resolvedDuration = (0, gift_checkout_offer_1.resolveGiftDuration)(input);
169
136
  let tier;
170
137
  try {
171
138
  tier = await this.deps.tiersService.api.read(input.tierId);
@@ -185,20 +152,15 @@ class GiftService {
185
152
  message: 'This tier is archived.',
186
153
  });
187
154
  }
188
- let duration = 1;
189
- let totalMonths;
190
- let amount = tier.getPrice(cadence);
191
- if (resolvedDuration) {
192
- const plan = (0, gift_checkout_offer_1.validateGiftCheckoutOffer)({
193
- tier,
194
- portalPlans: this.deps.settingsCache.get('portal_plans'),
195
- offer: resolvedDuration,
196
- });
197
- cadence = plan.cadence;
198
- duration = plan.duration;
199
- totalMonths = plan.totalMonths;
200
- amount = plan.amount;
201
- }
155
+ const plan = (0, gift_checkout_offer_1.validateGiftCheckoutOffer)({
156
+ tier,
157
+ portalPlans: this.deps.settingsCache.get('portal_plans'),
158
+ offer: resolvedDuration,
159
+ });
160
+ const cadence = plan.cadence;
161
+ const duration = plan.duration;
162
+ const totalMonths = plan.totalMonths;
163
+ const amount = plan.amount;
202
164
  const tierId = typeof tier.id === 'string' ? tier.id : tier.id.toHexString();
203
165
  const token = this.generateToken();
204
166
  const successUrl = new URL(input.successUrl);
@@ -34,7 +34,6 @@ function init(options) {
34
34
  const MailgunClient = require('../lib/mailgun-client');
35
35
  const config = require('../../../shared/config');
36
36
  const settingsCache = require('../../../shared/settings-cache');
37
- const labsService = require('../../../shared/labs');
38
37
  const urlUtils = require('../../../shared/url-utils').default;
39
38
  const settingsHelpers = require('../settings-helpers');
40
39
  const emailAddressParser = require('../email-address/email-address-parser');
@@ -110,7 +109,6 @@ function init(options) {
110
109
  },
111
110
  giftReminderScheduler,
112
111
  checkoutAdapter,
113
- labsService,
114
112
  settingsCache,
115
113
  });
116
114
  exports.service = giftService;
@@ -10,6 +10,14 @@ const errors_1 = __importDefault(require("@tryghost/errors"));
10
10
  const jobs_service_1 = require("./jobs-service");
11
11
  let instance;
12
12
  function init() {
13
+ // The instance lives for the whole process: the didInit-guarded mentions
14
+ // service captures it in MentionController and MentionSendingService, so
15
+ // an in-process restart (test harness) must revive the same object rather
16
+ // than strand those references on a stopped queue.
17
+ if (instance) {
18
+ instance.clearHandlers();
19
+ return instance;
20
+ }
13
21
  const adapterManager = require('../adapter-manager').default;
14
22
  const logging = require('@tryghost/logging');
15
23
  const sentry = require('../../../shared/sentry');
@@ -11,12 +11,14 @@ class JobsService {
11
11
  #logging;
12
12
  #sentry;
13
13
  #registry = new Map();
14
+ #queueByType = new Map();
15
+ #queues = new Map();
14
16
  constructor({ backend, logging, sentry }) {
15
17
  this.#backend = backend;
16
18
  this.#logging = logging;
17
19
  this.#sentry = sentry;
18
20
  }
19
- handle(JobClass, handler) {
21
+ handle(JobClass, handler, options) {
20
22
  const type = JobClass.type;
21
23
  if (typeof type !== 'string' || type.length === 0) {
22
24
  throw new errors_1.default.IncorrectUsageError({
@@ -28,14 +30,52 @@ class JobsService {
28
30
  message: `A handler for job type "${type}" is already registered.`,
29
31
  });
30
32
  }
33
+ this.#declareQueue(type, options);
31
34
  this.#registry.set(type, (payload) => handler(new JobClass(JSON.parse(payload))));
32
35
  }
36
+ #declareQueue(type, options) {
37
+ if (!options) {
38
+ return;
39
+ }
40
+ const { queue, concurrency } = options;
41
+ if (typeof queue !== 'string' || queue.length === 0) {
42
+ throw new errors_1.default.IncorrectUsageError({
43
+ message: `Invalid queue for job type "${type}": ${JSON.stringify(queue)}. Expected a non-empty string.`,
44
+ });
45
+ }
46
+ // "default" is the backend's shared lane for types that declare no queue;
47
+ // declaring it would silently re-size that lane for every unrouted type.
48
+ if (queue === 'default') {
49
+ throw new errors_1.default.IncorrectUsageError({
50
+ message: `Queue name "default" is reserved for the shared lane; job type "${type}" must omit options to use it.`,
51
+ });
52
+ }
53
+ if (!Number.isInteger(concurrency) || concurrency < 1) {
54
+ throw new errors_1.default.IncorrectUsageError({
55
+ message: `Invalid concurrency for job type "${type}": ${JSON.stringify(concurrency)}. Expected a positive integer.`,
56
+ });
57
+ }
58
+ const existing = this.#queues.get(queue);
59
+ if (existing && existing.concurrency !== concurrency) {
60
+ throw new errors_1.default.IncorrectUsageError({
61
+ message: `Conflicting concurrency for queue "${queue}": ${existing.concurrency} is already declared, job type "${type}" declares ${concurrency}.`,
62
+ });
63
+ }
64
+ this.#queues.set(queue, { concurrency });
65
+ this.#queueByType.set(type, queue);
66
+ }
67
+ #routingFor(type) {
68
+ const queue = this.#queueByType.get(type);
69
+ return queue === undefined ? undefined : { queue };
70
+ }
33
71
  async dispatch(job) {
34
- await this.#backend.enqueue(this.#buildEnvelope(job));
72
+ const envelope = this.#buildEnvelope(job);
73
+ await this.#backend.enqueue(envelope, this.#routingFor(envelope.type));
35
74
  }
36
75
  async scheduleRecurring(job, schedule) {
37
76
  this.#assertValidCron(schedule.cron);
38
- await this.#backend.scheduleRecurring(this.#buildEnvelope(job), schedule);
77
+ const envelope = this.#buildEnvelope(job);
78
+ await this.#backend.scheduleRecurring(envelope, schedule, this.#routingFor(envelope.type));
39
79
  }
40
80
  // later.parse.cron does not strictly validate: it silently coerces a
41
81
  // malformed expression into a bogus schedule (garbage -> every minute,
@@ -55,11 +95,20 @@ class JobsService {
55
95
  async start() {
56
96
  await this.#backend.start({
57
97
  processor: (envelope) => this.#process(envelope),
98
+ queues: Object.fromEntries(this.#queues),
58
99
  });
59
100
  }
60
101
  async shutdown(options) {
61
102
  await this.#backend.shutdown(options);
62
103
  }
104
+ // An in-process restart (test harness) re-runs handler registration on the
105
+ // same instance, so all registration state resets - handlers and queue
106
+ // declarations alike. The duplicate-type guard still holds within a boot.
107
+ clearHandlers() {
108
+ this.#registry.clear();
109
+ this.#queueByType.clear();
110
+ this.#queues.clear();
111
+ }
63
112
  #buildEnvelope(job) {
64
113
  return { type: this.#typeOf(job), payload: JSON.stringify(job) };
65
114
  }
@@ -45,8 +45,16 @@ const content_csv_import_job_1 = __importDefault(require("../content-import/jobs
45
45
  const contentImport = __importStar(require("../content-import"));
46
46
  const update_check_job_1 = __importDefault(require("../update-check/jobs/update-check-job"));
47
47
  const process_webmention_job_1 = __importDefault(require("../mentions/process-webmention-job"));
48
+ const send_webmentions_job_1 = __importDefault(require("../mentions/send-webmentions-job"));
48
49
  const updateCheck = require('../update-check');
49
- function registerJobHandlers({ jobsService, memberJobs, giftService, mediaInliner, mentionsController, }) {
50
+ // Webmention processing fetches external pages and is triggered by
51
+ // unauthenticated requests, so webmention jobs run in their own lane where a
52
+ // flood cannot occupy the shared workers. The concurrency matches the old
53
+ // dedicated mentions job queue. Every webmention job type must register with
54
+ // this shared declaration so none can declare the queue with a different
55
+ // concurrency.
56
+ const WEBMENTIONS_QUEUE = { queue: 'webmentions', concurrency: 3 };
57
+ function registerJobHandlers({ jobsService, memberJobs, giftService, mediaInliner, mentionsController, mentionsSendingService, }) {
50
58
  jobsService.handle(clean_tokens_job_1.default, async () => {
51
59
  await memberJobs.cleanTokens();
52
60
  });
@@ -67,5 +75,8 @@ function registerJobHandlers({ jobsService, memberJobs, giftService, mediaInline
67
75
  });
68
76
  jobsService.handle(process_webmention_job_1.default, async (job) => {
69
77
  await mentionsController.processWebmention(job);
70
- });
78
+ }, WEBMENTIONS_QUEUE);
79
+ jobsService.handle(send_webmentions_job_1.default, async (job) => {
80
+ await mentionsSendingService.sendWebmentions(job);
81
+ }, WEBMENTIONS_QUEUE);
71
82
  }