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.
- package/components/tryghost-adapter-base-jobs-0.0.0.tgz +0 -0
- package/components/tryghost-adapter-base-scheduling-0.2.3.tgz +0 -0
- package/components/tryghost-adapter-base-sso-0.1.3.tgz +0 -0
- package/components/tryghost-admin-api-schema-0.0.0.tgz +0 -0
- package/components/tryghost-checkout-0.0.0.tgz +0 -0
- package/components/tryghost-i18n-0.0.0.tgz +0 -0
- package/components/tryghost-kg-card-factory-5.2.4.tgz +0 -0
- package/components/tryghost-kg-clean-basic-html-4.3.4.tgz +0 -0
- package/components/tryghost-kg-converters-1.2.5.tgz +0 -0
- package/components/tryghost-kg-default-cards-10.3.5.tgz +0 -0
- package/components/tryghost-kg-default-nodes-2.2.1.tgz +0 -0
- package/components/tryghost-kg-default-transforms-1.3.4.tgz +0 -0
- package/components/tryghost-kg-html-to-lexical-1.4.0.tgz +0 -0
- package/components/tryghost-kg-lexical-html-renderer-1.5.0.tgz +0 -0
- package/components/tryghost-kg-markdown-html-renderer-7.2.4.tgz +0 -0
- package/components/tryghost-kg-utils-1.1.4.tgz +0 -0
- package/components/tryghost-limit-service-0.0.0.tgz +0 -0
- package/components/tryghost-metafield-types-0.0.0.tgz +0 -0
- package/content/themes/source/package.json +2 -2
- package/content/themes/source/partials/post-card.hbs +1 -1
- package/core/boot.js +7 -5
- package/core/built/admin/assets/{about-Dyae_UT6.js → about-C1C3s5-g.js} +1 -1
- package/core/built/admin/assets/accordion-CxGsDKwa.js +1 -0
- package/core/built/admin/assets/{account-migration-CESa0mnX.js → account-migration-CbUhW6gk.js} +1 -1
- package/core/built/admin/assets/activitypub/{account-migration-By69CgCp.js → account-migration-KA6Jjf9L.js} +7 -7
- package/core/built/admin/assets/activitypub/activitypub.js +3 -3
- package/core/built/admin/assets/activitypub/{avatar-DLPL3Abs.js → avatar-DVJF5bxP.js} +2 -2
- package/core/built/admin/assets/activitypub/{avatar-flipboard-C9iDY-QV.js → avatar-flipboard-csQaNeYK.js} +2 -2
- package/core/built/admin/assets/activitypub/{bluesky-sharing-BJcPMrDd.js → bluesky-sharing-C7nTDMvw.js} +9 -9
- package/core/built/admin/assets/activitypub/{content-formatters-3rTHg3mp.js → content-formatters-CLcjaBwV.js} +4 -4
- package/core/built/admin/assets/activitypub/{copy-DIAOQoCZ.js → copy-DcOh8kXw.js} +2 -2
- package/core/built/admin/assets/activitypub/{domain-DUR4xSkV.js → domain-fZ-X8i4-.js} +9 -9
- package/core/built/admin/assets/activitypub/{edit-profile-BqtfzxCp.js → edit-profile-BqMLQV88.js} +6 -6
- package/core/built/admin/assets/activitypub/{explore-BxuJd-J4.js → explore-B4yPoP8E.js} +10 -10
- package/core/built/admin/assets/activitypub/{feed-BM1TcksA.js → feed-BRC_mvAe.js} +9 -9
- package/core/built/admin/assets/activitypub/{field-Dn7HHx2-.js → field-COacNgOt.js} +3 -3
- package/core/built/admin/assets/activitypub/{hash-Bm3WxYN4.js → hash-C7u-irQQ.js} +2 -2
- package/core/built/admin/assets/activitypub/{inbox-CCjkuow2.js → inbox-BW-yvUoN.js} +10 -10
- package/core/built/admin/assets/activitypub/{inbox-d8oJRTf4.js → inbox-CbTTdDQ3.js} +2 -2
- package/core/built/admin/assets/activitypub/{label-1AA_s9Fa.js → label-kPMmYI7l.js} +3 -3
- package/core/built/admin/assets/activitypub/layout-BL4_jtuV.js +1 -0
- package/core/built/admin/assets/activitypub/{moderation-CnmxEB8w.js → moderation-CuidJzc0.js} +10 -10
- package/core/built/admin/assets/activitypub/{note-ZKc_Bnba.js → note-CAMRRSqD.js} +7 -7
- package/core/built/admin/assets/activitypub/{notifications-CB2tJZau.js → notifications-BmNlXGgp.js} +9 -9
- package/core/built/admin/assets/activitypub/{onboarding-BsJ9E5qf.js → onboarding-Bqg7fh_L.js} +2 -2
- package/core/built/admin/assets/activitypub/{onboarding-CSUJJIlh.js → onboarding-Dmun9jr_.js} +2 -2
- package/core/built/admin/assets/activitypub/{preferences-0-JZnUkI.js → preferences--oH19wf9.js} +11 -11
- package/core/built/admin/assets/activitypub/{profile-C35cFIhM.js → profile-DH_tQx2D.js} +14 -14
- package/core/built/admin/assets/activitypub/{reply-DYK5vVqb.js → reply-Cap9PxSH.js} +2 -2
- package/core/built/admin/assets/activitypub/{routes-BD4vscyc.js → routes-DdyY9uFF.js} +30 -30
- package/core/built/admin/assets/activitypub/{separator-BbQqTm-p.js → separator-CAxxu8VV.js} +3 -3
- package/core/built/admin/assets/activitypub/{settings-BsMROdqh.js → settings-B6EEcVkW.js} +6 -6
- package/core/built/admin/assets/activitypub/{site-O0RL9YCN.js → site-BYps63oh.js} +2 -2
- package/core/built/admin/assets/activitypub/{step-1-BMSx7c_X.js → step-1-CKjR2KAm.js} +6 -6
- package/core/built/admin/assets/activitypub/{step-2-DtPQzlTH.js → step-2-B6PKfOUQ.js} +6 -6
- package/core/built/admin/assets/activitypub/{step-3-CLlOZTyt.js → step-3-C-qBbcUo.js} +11 -11
- package/core/built/admin/assets/activitypub/{tabs-Dxk-b9M-.js → tabs-DLhZtVde.js} +6 -6
- package/core/built/admin/assets/activitypub/{topic-filter-5yZH6tId.js → topic-filter-DsUOuu0k.js} +3 -3
- package/core/built/admin/assets/activitypub/{use-activity-pub-queries-CV50qe03.js → use-activity-pub-queries-CX9snsLs.js} +3 -3
- package/core/built/admin/assets/activitypub/{use-navigate-with-base-path-Bmk5h4lW.js → use-navigate-with-base-path-DsJTGDSS.js} +988 -959
- package/core/built/admin/assets/activitypub/{use-reply-chain-data-Cbvzgjzd.js → use-reply-chain-data-AJi9zkzm.js} +5 -5
- package/core/built/admin/assets/activitypub/{users-BjcoP_HC.js → users-inweyKL5.js} +186 -160
- package/core/built/admin/assets/activitypub/{x-DbQZOLiI.js → x-DR8AT3-4.js} +2 -2
- package/core/built/admin/assets/{add-integration-modal-BlF6rs_f.js → add-integration-modal-BT_onDJy.js} +1 -1
- package/core/built/admin/assets/{add-newsletter-modal-CglVQqah.js → add-newsletter-modal-Cx1kQpiY.js} +1 -1
- package/core/built/admin/assets/{add-recommendation-modal-wz7BsuaG.js → add-recommendation-modal-B5nvAhvr.js} +1 -1
- package/core/built/admin/assets/{analytics-C4HE1gBg.js → analytics-DjoJWPI7.js} +1 -1
- package/core/built/admin/assets/{announcement-bar-modal-Ci4zz8jO.js → announcement-bar-modal-Dwd0EGFP.js} +1 -1
- package/core/built/admin/assets/{api-keys-3-0vJC3Y.js → api-keys-BGPzLHzA.js} +1 -1
- package/core/built/admin/assets/{api-keys-Ck-Yteiv.js → api-keys-BoeacM-s.js} +1 -1
- package/core/built/admin/assets/{audience-CtbA54Sn.js → audience-ceV3XVHq.js} +1 -1
- package/core/built/admin/assets/automated-emails-BLesE41L.js +1 -0
- package/core/built/admin/assets/{automations-sAcJ1334.js → automations-BN4u3GR8.js} +1 -1
- package/core/built/admin/assets/automations-C-ZTUwW3.js +1 -0
- package/core/built/admin/assets/{bluesky-sharing-BEtJ_gL_.js → bluesky-sharing-vzh-oPXL.js} +1 -1
- package/core/built/admin/assets/{breadcrumb-zUrZrBtW.js → breadcrumb-CaLk2b7D.js} +1 -1
- package/core/built/admin/assets/{button-eNMKQWXr.js → button-Dvcv1QSR.js} +1 -1
- package/core/built/admin/assets/calendar-Bl9RLcZX.js +1 -0
- package/core/built/admin/assets/{chart-helpers-CfBKt0vT.js → chart-helpers-FHywU0pj.js} +1 -1
- package/core/built/admin/assets/{checkbox-CgOn_tKW.js → checkbox-BtayFtjg.js} +1 -1
- package/core/built/admin/assets/chunk.254.035b85ec456ebbd7336b.js +440 -0
- package/core/built/admin/assets/{chunk.524.a697dee88db67fdf6021.js → chunk.524.e6975095fccdf22d90f8.js} +5 -5
- package/core/built/admin/assets/{chunk.582.5ed5d124cfca9c259b9c.js → chunk.582.98b485f9b2802f540897.js} +6 -6
- package/core/built/admin/assets/{chunk.492.f24d855d7e3f3aad261c.js → chunk.901.4ccc639821020230bcca.js} +9949 -7921
- package/core/built/admin/assets/{code-editor-view-K2wI-I5f.js → code-editor-view-C3ZfoF1X.js} +1 -1
- package/core/built/admin/assets/{code-editor-view-BYNoAoNV.js → code-editor-view-CAyRWy1s.js} +1 -1
- package/core/built/admin/assets/{color-picker-Ddw80xX6.js → color-picker-G4kGR7rp.js} +1 -1
- package/core/built/admin/assets/{color-picker-field-D9nUhvA7.js → color-picker-field-DQFVTNnT.js} +1 -1
- package/core/built/admin/assets/{combobox-w9j8PPQT.js → combobox-BnYzgEB4.js} +1 -1
- package/core/built/admin/assets/{command-BfpHORBv.js → command-C_vyTikI.js} +1 -1
- package/core/built/admin/assets/comments-aBrxfnZf.js +1 -0
- package/core/built/admin/assets/{config-DOMQDZGZ.js → config-kSHvwdTG.js} +1 -1
- package/core/built/admin/assets/{confirmation-context-CBM8G0Vj.js → confirmation-context-DwXX_Y5h.js} +1 -1
- package/core/built/admin/assets/{content-api-modal-TXRz3U7S.js → content-api-modal-D2G0nJP6.js} +1 -1
- package/core/built/admin/assets/{context-menu-_QMdd3kp.js → context-menu-DOVB6yaz.js} +1 -1
- package/core/built/admin/assets/{copy-field-6N01S07E.js → copy-field-DCJwh9qf.js} +1 -1
- package/core/built/admin/assets/{create-ghost-browse-value-source-qIBymvvf.js → create-ghost-browse-value-source-5C53jJaX.js} +1 -1
- package/core/built/admin/assets/custom-field-type-option-BAuW0F-E.js +1 -0
- package/core/built/admin/assets/{custom-integration-modal-DmNquxUV.js → custom-integration-modal-AbJGVyKc.js} +1 -1
- package/core/built/admin/assets/{date-range-select-uvz_DtPh.js → date-range-select-BY3K-ukj.js} +1 -1
- package/core/built/admin/assets/design-and-theme-modal-BvEy4_pC.js +2 -0
- package/core/built/admin/assets/{dirty-confirm-dialog-BZ1iytEd.js → dirty-confirm-dialog-DVPuGz3U.js} +1 -1
- package/core/built/admin/assets/{disabled-sources-indicator-DMRrxsiJ.js → disabled-sources-indicator-Cy7Zp4c8.js} +1 -1
- package/core/built/admin/assets/{domain-BNYrt0aD.js → domain-Dl61QGuk.js} +1 -1
- package/core/built/admin/assets/{dropzone-D7nFdAe5.js → dropzone-Dtz5Rmjt.js} +4 -4
- package/core/built/admin/assets/edit-profile-DNJSMDwm.js +1 -0
- package/core/built/admin/assets/editor-DbAen9zf.js +8 -0
- package/core/built/admin/assets/editor-screen-BFWThIai.js +26 -0
- package/core/built/admin/assets/email-sending-status-context-CWDOD1FN.js +1 -0
- package/core/built/admin/assets/email-sending-status-provider-DCPkDICt.js +1 -0
- package/core/built/admin/assets/emails-vFMk4yYl.js +1 -0
- package/core/built/admin/assets/{embed-signup-form-modal-Ds6mPEi5.js → embed-signup-form-modal-DGqsORxS.js} +1 -1
- package/core/built/admin/assets/{explore-UY10Egsp.js → explore-nnRr1Fja.js} +1 -1
- package/core/built/admin/assets/feature-flag-overrides-context-DQjdJFAk.js +1 -0
- package/core/built/admin/assets/{feature-image-placeholder-D1iF_8bc.js → feature-image-placeholder-BrIHmPs8.js} +1 -1
- package/core/built/admin/assets/{feed-81C8mJWR.js → feed-BNUcNu-W.js} +1 -1
- package/core/built/admin/assets/feedback-DJ4Zb9rZ.js +1 -0
- package/core/built/admin/assets/{field-Dihp5p18.js → field-C93_Mg6j.js} +1 -1
- package/core/built/admin/assets/filter-sources-CGJb4C5r.js +1 -0
- package/core/built/admin/assets/{filters-DirqNXjH.js → filters--kKP-EC8.js} +1 -1
- package/core/built/admin/assets/filters-DxA_a4FP.js +1 -0
- package/core/built/admin/assets/{first-promoter-modal-CT6MxYZb.js → first-promoter-modal-BYDtrz2S.js} +1 -1
- package/core/built/admin/assets/focus-koenig-editor-on-bottom-click-BbSnGOtK.js +1 -0
- package/core/built/admin/assets/{ghost-8eabfc1ece81b03cf7b90780d31487e8.css → ghost-8fd5bb799356bbefb254210ba7e1236d.css} +1 -1
- package/core/built/admin/assets/{ghost-c2848508292937415f20f5a830eea59e.js → ghost-b4e8a403baba61905cd7560e30b9732d.js} +43 -27
- package/core/built/admin/assets/{ghost-dark-a7bebe2c561f55369dcf29f4c820f7ff.css → ghost-dark-cac0cb2d5bb78e9bd14d8d1c2768ed45.css} +1 -1
- package/core/built/admin/assets/{gift-link-modal-CQVAye17.js → gift-link-modal-B7u5bD4e.js} +1 -1
- package/core/built/admin/assets/{gift-links-DLDS7ljO.js → gift-links-CfLSnst2.js} +1 -1
- package/core/built/admin/assets/growth-Cd1uO9d2.js +1 -0
- package/core/built/admin/assets/growth-aG32xfdE.js +3 -0
- package/core/built/admin/assets/{history-modal-CFd1Hvx6.js → history-modal-Df8lW8h-.js} +1 -1
- package/core/built/admin/assets/hooks-Cy_-oZ4o.js +2 -0
- package/core/built/admin/assets/{html-field-CJBCLTvv.js → html-field-DA4eU7eq.js} +1 -1
- package/core/built/admin/assets/{images-scfkWZiK.js → images-KXmyO3Ss.js} +1 -1
- package/core/built/admin/assets/{inbox-Mcp4DXZN.js → inbox-gJHG7_xd.js} +6 -6
- package/core/built/admin/assets/index-BHHXtR5C.css +2 -0
- package/core/built/admin/assets/index-CgMCwsxN.js +4 -0
- package/core/built/admin/assets/index.es-CY6-yuw9.js +1 -0
- package/core/built/admin/assets/{input-group-CMhe2DvY.js → input-group-BE8rM2y1.js} +1 -1
- package/core/built/admin/assets/{integrations-Bjbu86Z0.js → integrations-B4kuRBFY.js} +1 -1
- package/core/built/admin/assets/{invite-user-modal-DfheMXs0.js → invite-user-modal-B4CuNcx4.js} +1 -1
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +2 -2
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +2 -2
- package/core/built/admin/assets/{koenig-lexical-DBSqAuiI.js → koenig-lexical-CGXb0F8o.js} +2 -2
- package/core/built/admin/assets/koenig-loader-BeM5rFPX.js +2 -0
- package/core/built/admin/assets/{kpi-card-DBRMfGOW.js → kpi-card-B21aToe8.js} +1 -1
- package/core/built/admin/assets/{kpi-card-DFafD1qq.js → kpi-card-pz6Wv4Ae.js} +1 -1
- package/core/built/admin/assets/{kpi-helpers-Cv8i8hns.js → kpi-helpers-Dq5jelmP.js} +1 -1
- package/core/built/admin/assets/{kpi-tabs-BJItvLYm.js → kpi-tabs-DEZHpKUt.js} +1 -1
- package/core/built/admin/assets/{label-BnSCvffu.js → label-OLCzze8N.js} +1 -1
- package/core/built/admin/assets/label-picker-BigOvMPV.js +1 -0
- package/core/built/admin/assets/{labels-CH3Kn8RV.js → labels-Dr0SgUQD.js} +1 -1
- package/core/built/admin/assets/layout-DUTL8SH5.js +1 -0
- package/core/built/admin/assets/lib-7QPTI0FS.js +1 -0
- package/core/built/admin/assets/link-helpers-BRAg9SCj.js +1 -0
- package/core/built/admin/assets/links-BDHPTlyj.js +1 -0
- package/core/built/admin/assets/{member-custom-fields-DDQV6QeG.js → member-custom-fields-TRTHCAz9.js} +1 -1
- package/core/built/admin/assets/member-detail-b6dRwc3C.js +1 -0
- package/core/built/admin/assets/members-Czt5lJSc.js +4 -0
- package/core/built/admin/assets/{moderation-AgCIEso4.js → moderation-ZWefD9uH.js} +1 -1
- package/core/built/admin/assets/{multi-select-combobox-BcITkUVr.js → multi-select-combobox-CT13MTo-.js} +1 -1
- package/core/built/admin/assets/navigation-modal-C8FEhBK5.js +1 -0
- package/core/built/admin/assets/newsletter-5VYW4RyJ.js +1 -0
- package/core/built/admin/assets/newsletter-detail-modal-ChjC0ZqH.js +1 -0
- package/core/built/admin/assets/{newsletter-emails-DKzwafuh.js → newsletter-emails-DZH6I-PY.js} +1 -1
- package/core/built/admin/assets/{newsletters-BRBb_uQC.js → newsletters-DOC9KBcX.js} +1 -1
- package/core/built/admin/assets/newsletters-Rq1M3GD-.js +1 -0
- package/core/built/admin/assets/{note-DyGHtS9a.js → note-DIV-Pojr.js} +1 -1
- package/core/built/admin/assets/{notifications-ZgWniKna.js → notifications-s20-H49V.js} +1 -1
- package/core/built/admin/assets/{offers-9J1CSiLy.js → offers--e2nGvkW.js} +1 -1
- package/core/built/admin/assets/{offers-container-modal-B-E8TIWr.js → offers-container-modal-BmG5M5Tv.js} +1 -1
- package/core/built/admin/assets/onboarding-CUuy9unR.js +1 -0
- package/core/built/admin/assets/{onboarding-CzIsre-d.js → onboarding-pXyct0Jm.js} +1 -1
- package/core/built/admin/assets/{onboarding-route-CaavZulg.js → onboarding-route-CK4pcXoR.js} +1 -1
- package/core/built/admin/assets/overview-CaPsciMs.js +1 -0
- package/core/built/admin/assets/overview-a8GfcsKX.js +1 -0
- package/core/built/admin/assets/{page-header-De0VzKBi.js → page-header-DM_KyoFI.js} +1 -1
- package/core/built/admin/assets/{pagemenu-B5KKCpbA.js → pagemenu-vgZVUVKR.js} +1 -1
- package/core/built/admin/assets/pages-CjDY2CBI.js +1 -0
- package/core/built/admin/assets/pages-route-BzKZYdV5.js +1 -0
- package/core/built/admin/assets/{pintura-modal-BZ2J9w3n.js → pintura-modal-DExCF1ii.js} +1 -1
- package/core/built/admin/assets/{portal-frame-B6Hr9wHc.js → portal-frame-Cqanidq4.js} +1 -1
- package/core/built/admin/assets/portal-modal-oinBgA27.js +1 -0
- package/core/built/admin/assets/post-analytics-C5oJ1dWV.js +1 -0
- package/core/built/admin/assets/post-analytics-header-OATo3X_r.js +1 -0
- package/core/built/admin/assets/post-analytics-provider-tOpIVGT5.js +1 -0
- package/core/built/admin/assets/post-helpers-C88JBa4D.js +1 -0
- package/core/built/admin/assets/post-share-modal-DvhC1XgV.js +1 -0
- package/core/built/admin/assets/post-utils-DKF4mfuo.js +1 -0
- package/core/built/admin/assets/posts-list-screen-o4ZjbgS3.js +2 -0
- package/core/built/admin/assets/posts-pCPrhQeR.js +1 -0
- package/core/built/admin/assets/posts-route-9F--xdIb.js +1 -0
- package/core/built/admin/assets/{preferences-C5YJLp74.js → preferences--vNubsqD.js} +2 -2
- package/core/built/admin/assets/{preview-modal-BfeFqi5y.js → preview-modal-YYbzlYdO.js} +1 -1
- package/core/built/admin/assets/profile-BllKV3dR.js +1 -0
- package/core/built/admin/assets/radio-group-DL8GI8rt.js +1 -0
- package/core/built/admin/assets/{referrers-CnhCMWCs.js → referrers-D8oHPmDB.js} +1 -1
- package/core/built/admin/assets/router-provider-BSvTXgZY.js +1 -0
- package/core/built/admin/assets/schemas-BoYdB6YU.js +64 -0
- package/core/built/admin/assets/{select-cb0t_TAJ.js → select-v5YbA3Mw.js} +1 -1
- package/core/built/admin/assets/settings-PEsZupw1.js +4 -0
- package/core/built/admin/assets/{settings-breadcrumbs-DLzbRRSt.js → settings-breadcrumbs-NwPnCTC7.js} +1 -1
- package/core/built/admin/assets/{settings-DZqxR0Da.js → settings-u_s1CRkr.js} +1 -1
- package/core/built/admin/assets/{share-modal-D2-Bz1_I.js → share-modal-SskCmVGL.js} +1 -1
- package/core/built/admin/assets/{slack-modal-DYDTUria.js → slack-modal-XxSJiYgo.js} +1 -1
- package/core/built/admin/assets/{sort-button-VVeQjjbu.js → sort-button-Cx1KtINE.js} +1 -1
- package/core/built/admin/assets/{source-icon-Ck5_5Kgq.js → source-icon-Bdp9Im_5.js} +1 -1
- package/core/built/admin/assets/stats-D9PDLS3x.js +1 -0
- package/core/built/admin/assets/stats-view-W2g6Jsd-.js +1 -0
- package/core/built/admin/assets/{step-1-CsaBjeN4.js → step-1-BQYzvBzr.js} +1 -1
- package/core/built/admin/assets/{step-2-D2vnxOOM.js → step-2-xES32xJV.js} +1 -1
- package/core/built/admin/assets/{step-3-wBoLdGEb.js → step-3-DA_XK5kW.js} +2 -2
- package/core/built/admin/assets/{stripe-button-CDgLz0S0.js → stripe-button-CVsLHelt.js} +1 -1
- package/core/built/admin/assets/{stripe-connect-modal-CSjMJetP.js → stripe-connect-modal-ClmLOMGS.js} +1 -1
- package/core/built/admin/assets/style-Wd9nQubI.css +1 -0
- package/core/built/admin/assets/{table-Do6iQ83Q.js → table-BenqVupI.js} +1 -1
- package/core/built/admin/assets/{tabs-D2yWLMp_.js → tabs-eBBlxPp8.js} +1 -1
- package/core/built/admin/assets/tag-detail-Ce8Se2qd.js +2 -0
- package/core/built/admin/assets/tag-picker-C3UR-uJ-.js +1 -0
- package/core/built/admin/assets/tags-B-kJxCs7.js +1 -0
- package/core/built/admin/assets/{tags-zTFiKRpb.js → tags-ChfdwJyS.js} +1 -1
- package/core/built/admin/assets/{testimonials-modal-CtnZBJKl.js → testimonials-modal-9Q6fT06V.js} +1 -1
- package/core/built/admin/assets/tier-detail-modal-CDyYdqK7.js +1 -0
- package/core/built/admin/assets/tiers-BCpIOGQr.js +1 -0
- package/core/built/admin/assets/{toggle-group-ByUJsxwD.js → toggle-group-CviIkyok.js} +1 -1
- package/core/built/admin/assets/{topic-filter-BgC3g5_v.js → topic-filter-I2jCBGxm.js} +1 -1
- package/core/built/admin/assets/{transistor-modal-Drr0SNfo.js → transistor-modal-p53u3W8y.js} +1 -1
- package/core/built/admin/assets/{trend-badge-6ye93YAW.js → trend-badge-CideVzQd.js} +1 -1
- package/core/built/admin/assets/{unsplash-modal-p6AoQoWp.js → unsplash-modal-DY8UcKcE.js} +1 -1
- package/core/built/admin/assets/{use-analytics-data-CWCMAESo.js → use-analytics-data-DAIGcr2f.js} +1 -1
- package/core/built/admin/assets/{use-check-theme-limit-error-Cw3WQHy8.js → use-check-theme-limit-error-DRUjK4YM.js} +1 -1
- package/core/built/admin/assets/use-filter-params-JkZhIS8L.js +1 -0
- package/core/built/admin/assets/{use-filterable-api-CzXcpUh8.js → use-filterable-api-BVEVMrhZ.js} +1 -1
- package/core/built/admin/assets/{use-growth-stats-DCVG3s9w.js → use-growth-stats-937CUj6j.js} +1 -1
- package/core/built/admin/assets/use-host-limits-D0Dv3tKA.js +1 -0
- package/core/built/admin/assets/{use-limiter-D2WRRx0H.js → use-limiter-CohWfzvK.js} +1 -1
- package/core/built/admin/assets/{use-navigate-with-base-path-BHPA228f.js → use-navigate-with-base-path-BkxcPj5l.js} +1 -1
- package/core/built/admin/assets/use-pintura-config-xgytjU35.js +1 -0
- package/core/built/admin/assets/{use-pintura-editor-BfN1UwzH.js → use-pintura-editor-CaV1mbfO.js} +1 -1
- package/core/built/admin/assets/{use-post-referrers-Czg47m8s.js → use-post-referrers-BCQI7fDg.js} +1 -1
- package/core/built/admin/assets/{use-reply-chain-data-ns0caK8k.js → use-reply-chain-data-CYRhoaxY.js} +1 -1
- package/core/built/admin/assets/{use-router-history-entry-uldn7zJO.js → use-router-history-entry-NOKcd--l.js} +1 -1
- package/core/built/admin/assets/{use-setting-group-DpcuCUDg.js → use-setting-group-bLKMwoBT.js} +1 -1
- package/core/built/admin/assets/{use-settings-navigation-CZjGrhsp.js → use-settings-navigation-B-qrriKD.js} +1 -1
- package/core/built/admin/assets/{use-simple-pagination-DzJWobPR.js → use-simple-pagination-CNPd6Rf6.js} +1 -1
- package/core/built/admin/assets/use-staff-users-pRIMIyiW.js +1 -0
- package/core/built/admin/assets/{use-tinybird-query-CSnGXzZh.js → use-tinybird-query-CmdbCS9X.js} +1 -1
- package/core/built/admin/assets/{use-tinybird-token-DTOKRR7e.js → use-tinybird-token-KLne235s.js} +1 -1
- package/core/built/admin/assets/{use-unsaved-changes-guard-CBzs-KPi.js → use-unsaved-changes-guard-7-ug0JdH.js} +1 -1
- package/core/built/admin/assets/use-upgrade-route-Dm_qqKr0.js +1 -0
- package/core/built/admin/assets/{use-url-input-DFr8lCbS.js → use-url-input-fvp-TpPx.js} +1 -1
- package/core/built/admin/assets/user-detail-modal-D3KlGYv-.js +1 -0
- package/core/built/admin/assets/{users-Cg-OeGzC.js → users-DVNWrqg9.js} +1 -1
- package/core/built/admin/assets/{vendor-5753232c7d6ffb3c44e1eb0a02133953.js → vendor-2dd263291938d407802568d3357ec69e.js} +19 -17
- package/core/built/admin/assets/{virtual-list-BW-padMe.js → virtual-list-CZ3RK2Yf.js} +1 -1
- package/core/built/admin/assets/{web-C1v33ehA.js → web-ClEJsCAj.js} +2 -2
- package/core/built/admin/assets/web-X9rdnrtq.js +1 -0
- package/core/built/admin/assets/{zapier-modal-BBFfI6Ml.js → zapier-modal-DfVhV17j.js} +1 -1
- package/core/built/admin/index.html +19 -18
- package/core/server/adapters/jobs/InMemoryJobsBackend.js +74 -29
- package/core/server/api/endpoints/authentication.js +1 -2
- package/core/server/api/endpoints/authors-public.js +15 -5
- package/core/server/api/endpoints/emails.js +13 -0
- package/core/server/api/endpoints/index.js +8 -0
- package/core/server/api/endpoints/member-metafields-members.js +23 -0
- package/core/server/api/endpoints/member-metafields.js +33 -20
- package/core/server/api/endpoints/members-account.js +42 -0
- package/core/server/api/endpoints/newsletters-public.js +10 -1
- package/core/server/api/endpoints/pages-public.js +15 -5
- package/core/server/api/endpoints/posts-public.js +15 -5
- package/core/server/api/endpoints/tags-public.js +22 -3
- package/core/server/api/endpoints/tiers-checkout-config.js +2 -2
- package/core/server/api/endpoints/tiers-public.js +38 -1
- package/core/server/api/endpoints/users.js +2 -2
- package/core/server/api/endpoints/utils/api-filter-utils.js +170 -3
- package/core/server/api/endpoints/utils/public-cache-keys.js +3 -0
- package/core/server/api/endpoints/utils/serializers/input/authors.js +2 -0
- package/core/server/api/endpoints/utils/serializers/input/members.js +16 -2
- package/core/server/api/endpoints/utils/serializers/input/pages.js +2 -0
- package/core/server/api/endpoints/utils/serializers/input/posts.js +2 -0
- package/core/server/api/endpoints/utils/serializers/input/tags.js +2 -0
- package/core/server/api/endpoints/utils/serializers/output/emails.js +7 -0
- package/core/server/api/endpoints/utils/serializers/output/index.js +4 -0
- package/core/server/api/endpoints/utils/serializers/output/member-metafields.js +3 -3
- package/core/server/api/endpoints/utils/serializers/output/members-account.js +19 -0
- package/core/server/api/endpoints/utils/serializers/output/members-csv-transform.js +1 -1
- package/core/server/api/endpoints/utils/serializers/output/tiers.js +10 -2
- package/core/server/api/endpoints/utils/serializers/output/users.js +0 -10
- package/core/server/data/exporter/table-lists.js +3 -3
- package/core/server/data/migrations/versions/6.53/2026-07-15-20-22-53-add-members-custom-field-values.js +1 -1
- package/core/server/data/migrations/versions/6.57/2026-08-04-18-00-00-store-custom-field-values-as-leaf-rows.js +10 -1
- package/core/server/data/migrations/versions/6.63/2026-09-03-10-07-03-rename-custom-field-tables-to-metafields.js +169 -0
- package/core/server/data/migrations/versions/6.63/2026-09-03-11-03-31-remove-member-custom-field-read-permissions.js +30 -0
- package/core/server/data/schema/fixtures/fixtures.json +0 -11
- package/core/server/data/schema/lib/default-index-name.js +8 -11
- package/core/server/data/schema/schema.js +18 -18
- package/core/server/data/seeders/README.md +48 -0
- package/core/server/data/tinybird/pipes/mv_hits.pipe +86 -50
- package/core/server/lib/db-types/count.js +16 -0
- package/core/server/lib/validate-password.js +81 -120
- package/core/server/models/action.js +4 -4
- package/core/server/models/base/plugins/filtered-collection.js +16 -0
- package/core/server/models/member.js +6 -6
- package/core/server/models/user.js +1 -1
- package/core/server/services/automations/automations-api.js +1 -1
- package/core/server/services/automations/database-automations-repository.js +11 -4
- package/core/server/services/email-service/CONTEXT.md +37 -0
- package/core/server/services/email-service/README.md +42 -0
- package/core/server/services/email-service/batch-sending-service.js +2 -0
- package/core/server/services/email-service/email-controller.js +18 -2
- package/core/server/services/email-service/email-service-wrapper.js +3 -0
- package/core/server/services/email-service/sending-status-schema.js +21 -0
- package/core/server/services/email-service/sending-status-serializers.js +33 -0
- package/core/server/services/email-service/sending-status-service.js +47 -0
- package/core/server/services/email-service/sending-status.js +111 -0
- package/core/server/services/email-suppression-list/email-suppression-list.js +0 -33
- package/core/server/services/email-suppression-list/mailgun-email-suppression-list.js +6 -7
- package/core/server/services/gifts/README.md +2 -2
- package/core/server/services/gifts/constants.js +1 -1
- package/core/server/services/gifts/gift-service.js +19 -57
- package/core/server/services/gifts/index.js +0 -2
- package/core/server/services/jobs-service/index.js +8 -0
- package/core/server/services/jobs-service/jobs-service.js +52 -3
- package/core/server/services/jobs-service/register-job-handlers.js +13 -2
- package/core/server/services/members/account-service.js +98 -0
- package/core/server/services/members/api.js +4 -4
- package/core/server/services/members/import-export/export/exporter.js +20 -20
- package/core/server/services/members/import-export/import/completion-email.js +6 -6
- package/core/server/services/members/import-export/import/importer.js +13 -13
- package/core/server/services/members/import-export/index.js +7 -7
- package/core/server/services/members/members-api/members-api.js +26 -6
- package/core/server/services/members/members-api/repositories/member-repository.js +4 -4
- package/core/server/services/members/members-api/services/member-bread-service.js +45 -39
- package/core/server/services/members/members-ssr.js +18 -0
- package/core/server/services/members/middleware.js +95 -81
- package/core/server/services/members/service.js +70 -42
- package/core/server/services/members/utils.js +9 -0
- package/core/server/services/members-metafields/access.js +28 -0
- package/core/server/services/{members-custom-fields → members-metafields}/actions.js +5 -5
- package/core/server/services/members-metafields/bindings-service.js +147 -0
- package/core/server/services/{members-custom-fields → members-metafields}/codec.js +4 -4
- package/core/server/services/{members-custom-fields → members-metafields}/config.js +1 -1
- package/core/server/services/{members-custom-fields → members-metafields}/definitions-service.js +17 -23
- package/core/server/services/{members-custom-fields → members-metafields}/filter.js +9 -9
- package/core/server/services/{members-custom-fields → members-metafields}/index.js +16 -7
- package/core/server/services/{members-custom-fields → members-metafields}/key.js +1 -1
- package/core/server/services/{members-custom-fields → members-metafields}/models.js +4 -4
- package/core/server/services/members-metafields/namespaces.js +34 -0
- package/core/server/services/{members-custom-fields → members-metafields}/queries.js +1 -1
- package/core/server/services/{members-custom-fields → members-metafields}/schema.js +16 -12
- package/core/server/services/{members-custom-fields → members-metafields}/serializers.js +6 -6
- package/core/server/services/{members-custom-fields → members-metafields}/values-service.js +32 -22
- package/core/server/services/mentions/mention-sending-service.js +26 -9
- package/core/server/services/mentions/send-webmentions-job.js +18 -0
- package/core/server/services/mentions/service.js +1 -31
- package/core/server/services/stripe/services/webhook/checkout-session-event-service.js +2 -2
- package/core/server/services/stripe/stripe-service.js +4 -4
- package/core/server/services/tier-checkout-config/codec.js +2 -2
- package/core/server/services/tier-checkout-config/index.js +2 -1
- package/core/server/services/tier-checkout-config/models.js +2 -2
- package/core/server/services/tier-checkout-config/queries.js +8 -8
- package/core/server/services/tier-checkout-config/serializers.js +57 -0
- package/core/server/services/tier-checkout-config/service.js +1 -1
- package/core/server/services/tiers/service.js +3 -3
- package/core/server/services/url/router-filter.js +8 -10
- package/core/server/services/users.js +1 -19
- package/core/server/web/api/endpoints/admin/routes.js +1 -0
- package/core/server/web/members/account/index.js +2 -0
- package/core/server/web/members/account/routes.js +46 -0
- package/core/server/web/members/app.js +4 -18
- package/core/shared/config/defaults.json +1 -1
- package/core/shared/labs.js +4 -2
- package/package.json +22 -22
- package/pnpm-lock.yaml +513 -571
- package/pnpm-workspace.yaml +12 -12
- package/components/tryghost-custom-field-types-0.0.0.tgz +0 -0
- package/core/built/admin/assets/activitypub/layout-2-0bw6HN.js +0 -1
- package/core/built/admin/assets/automated-emails-D5VVMND9.js +0 -1
- package/core/built/admin/assets/automations-CzKt5fb8.js +0 -1
- package/core/built/admin/assets/chunk.598.7e056fabb47ce7dbd73f.js +0 -366
- package/core/built/admin/assets/comments-BLmCmyCs.js +0 -1
- package/core/built/admin/assets/custom-field-type-option-BddFlJFa.js +0 -1
- package/core/built/admin/assets/design-and-theme-modal-BSmo12Op.js +0 -2
- package/core/built/admin/assets/edit-profile-C5Asm4LA.js +0 -1
- package/core/built/admin/assets/editor-REokrzci.js +0 -8
- package/core/built/admin/assets/filter-sources-BO9xUPj7.js +0 -1
- package/core/built/admin/assets/filters-CRUWecfY.js +0 -1
- package/core/built/admin/assets/growth-BWIu-6xj.js +0 -3
- package/core/built/admin/assets/growth-DU29oxeA.js +0 -1
- package/core/built/admin/assets/hooks-CuEZV2-q.js +0 -2
- package/core/built/admin/assets/index-6MkPdCtr.js +0 -4
- package/core/built/admin/assets/index-Bn7QhcZu.css +0 -2
- package/core/built/admin/assets/index.es-CNnuNH0d.js +0 -1
- package/core/built/admin/assets/koenig-loader-aFQNlJlD.js +0 -2
- package/core/built/admin/assets/label-picker-KVoo91sh.js +0 -1
- package/core/built/admin/assets/layout-Bjv2Ll-5.js +0 -1
- package/core/built/admin/assets/link-helpers-MVDdnFCY.js +0 -1
- package/core/built/admin/assets/member-detail-CdJF6Agw.js +0 -1
- package/core/built/admin/assets/members-DjQbq9Z2.js +0 -4
- package/core/built/admin/assets/navigation-modal-BkT7b1M2.js +0 -1
- package/core/built/admin/assets/newsletter-BMMAuvgq.js +0 -1
- package/core/built/admin/assets/newsletter-detail-modal-0SCJ7Zpm.js +0 -1
- package/core/built/admin/assets/newsletters-DhNigJUh.js +0 -1
- package/core/built/admin/assets/onboarding-qg8Akovm.js +0 -1
- package/core/built/admin/assets/overview-BW8GLeYi.js +0 -1
- package/core/built/admin/assets/overview-BkXZNzaZ.js +0 -1
- package/core/built/admin/assets/pages-jgaFm69d.js +0 -1
- package/core/built/admin/assets/pages-route-CIUAQxtY.js +0 -1
- package/core/built/admin/assets/portal-modal-D4ceei3t.js +0 -1
- package/core/built/admin/assets/post-analytics-header-BYCsT0GP.js +0 -1
- package/core/built/admin/assets/post-analytics-ot7f6pih.js +0 -1
- package/core/built/admin/assets/post-analytics-provider-Bo1gmYaP.js +0 -1
- package/core/built/admin/assets/post-helpers-C5OQMBoi.js +0 -1
- package/core/built/admin/assets/post-share-modal-C-PaYZ_e.js +0 -1
- package/core/built/admin/assets/posts-DuGazgTb.js +0 -1
- package/core/built/admin/assets/posts-list-screen-CBGGgjjj.js +0 -2
- package/core/built/admin/assets/posts-route-D6xmS7Vf.js +0 -1
- package/core/built/admin/assets/profile-B8mx2yAu.js +0 -1
- package/core/built/admin/assets/radio-group-DczlLKOn.js +0 -1
- package/core/built/admin/assets/router-provider-B0gZyiGJ.js +0 -1
- package/core/built/admin/assets/schemas-DppqmpeJ.js +0 -64
- package/core/built/admin/assets/settings-DvfTNdHQ.js +0 -4
- package/core/built/admin/assets/stats-Rjmk5owg.js +0 -1
- package/core/built/admin/assets/stats-view-BGGmkmgv.js +0 -1
- package/core/built/admin/assets/tag-detail-BhXlOsCa.js +0 -2
- package/core/built/admin/assets/tags-ldukRApe.js +0 -1
- package/core/built/admin/assets/tier-detail-modal-BOs8e_G8.js +0 -1
- package/core/built/admin/assets/tiers-Nd_z4BZY.js +0 -1
- package/core/built/admin/assets/use-filter-params-BIni9taJ.js +0 -1
- package/core/built/admin/assets/use-host-limits-DZtR08oa.js +0 -1
- package/core/built/admin/assets/use-pintura-config-h-J0Lvta.js +0 -1
- package/core/built/admin/assets/use-staff-users-Csipb-O0.js +0 -1
- package/core/built/admin/assets/use-upgrade-route-DhRSo7at.js +0 -1
- package/core/built/admin/assets/user-detail-modal-B_iUClI6.js +0 -1
- package/core/built/admin/assets/web-C4BKzn2T.js +0 -1
- package/core/server/services/email-suppression-list/in-memory-email-suppression-list.js +0 -39
- package/core/server/services/members-custom-fields/bindings-service.js +0 -120
- /package/core/built/admin/assets/{chunk.492.f24d855d7e3f3aad261c.js.LICENSE.txt → chunk.901.4ccc639821020230bcca.js.LICENSE.txt} +0 -0
- /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
|
-
|
|
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
|
|
10
|
-
|
|
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
|
|
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 (
|
|
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(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|