ghost 6.68.0 → 6.69.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/ghost-storage-base-3.0.1.tgz +0 -0
- package/components/tryghost-adapter-base-cache-0.2.1.tgz +0 -0
- package/components/tryghost-adapter-base-scheduling-0.2.5.tgz +0 -0
- package/components/tryghost-adapter-base-sso-0.1.4.tgz +0 -0
- package/components/tryghost-app-contracts-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.6.tgz +0 -0
- package/components/tryghost-kg-clean-basic-html-4.3.6.tgz +0 -0
- package/components/tryghost-kg-converters-1.2.6.tgz +0 -0
- package/components/tryghost-kg-default-cards-10.3.7.tgz +0 -0
- package/components/tryghost-kg-default-nodes-2.2.3.tgz +0 -0
- package/components/tryghost-kg-default-transforms-1.3.6.tgz +0 -0
- package/components/tryghost-kg-html-to-lexical-1.4.1.tgz +0 -0
- package/components/tryghost-kg-lexical-html-renderer-1.5.1.tgz +0 -0
- package/components/tryghost-kg-markdown-html-renderer-7.2.7.tgz +0 -0
- package/components/tryghost-kg-utils-1.1.6.tgz +0 -0
- package/components/tryghost-metafield-csv-0.0.0.tgz +0 -0
- package/components/tryghost-metafield-types-0.1.1.tgz +0 -0
- package/core/boot.js +6 -0
- package/core/built/admin/assets/{about-D_nwtf7X.js → about-Dxp30cNg.js} +1 -1
- package/core/built/admin/assets/{account-migration-BaecWw1T.js → account-migration-D05akW4q.js} +1 -1
- package/core/built/admin/assets/activitypub/{account-migration-4saTEZ6p.js → account-migration-BmiwuzXI.js} +6 -6
- package/core/built/admin/assets/activitypub/activitypub.js +2 -2
- package/core/built/admin/assets/activitypub/{bluesky-sharing-BHBNuZQH.js → bluesky-sharing-B1fzMdFb.js} +6 -6
- package/core/built/admin/assets/activitypub/{content-formatters-DVx9YktY.js → content-formatters-G0qBT5QE.js} +3 -3
- package/core/built/admin/assets/activitypub/{domain-rAmlMXBC.js → domain-B3UtdA1m.js} +7 -7
- package/core/built/admin/assets/activitypub/{edit-profile-CXYL4ZrS.js → edit-profile-9EodzviR.js} +4 -4
- package/core/built/admin/assets/activitypub/{explore-dqpEx3GA.js → explore-DmwPdxHI.js} +8 -8
- package/core/built/admin/assets/activitypub/{feed-Cb-wYNw9.js → feed-yauqnu7A.js} +5 -5
- package/core/built/admin/assets/activitypub/{field-JxegcPt6.js → field-CsrMjbg2.js} +2 -2
- package/core/built/admin/assets/activitypub/{inbox-2hXDJBn5.js → inbox-DV3RrpFH.js} +7 -7
- package/core/built/admin/assets/activitypub/{label-DRQzZ7lb.js → label-DPs40NLn.js} +2 -2
- package/core/built/admin/assets/activitypub/layout-CvOhndCA.js +1 -0
- package/core/built/admin/assets/activitypub/{moderation-BnYKcTXY.js → moderation-DGa6hVNH.js} +7 -7
- package/core/built/admin/assets/activitypub/{note-C6ucS3Wu.js → note-BVL6LVHF.js} +5 -5
- package/core/built/admin/assets/activitypub/{notifications-DgBW9wEC.js → notifications-CRUu_uMC.js} +5 -5
- package/core/built/admin/assets/activitypub/{onboarding-DGu6yG4L.js → onboarding-Dpm-nLkQ.js} +2 -2
- package/core/built/admin/assets/activitypub/{onboarding-Bq3fNf_a.js → onboarding-KtpJA-xb.js} +2 -2
- package/core/built/admin/assets/activitypub/{preferences-C3jI1eVN.js → preferences-CdFuVqIt.js} +7 -7
- package/core/built/admin/assets/activitypub/{profile-Cn3vRb5Z.js → profile-KnNAoHYu.js} +8 -8
- package/core/built/admin/assets/activitypub/{routes-DbrwlS9h.js → routes-BRndai50.js} +26 -26
- package/core/built/admin/assets/activitypub/{settings-D0BALDQu.js → settings-DU7ftJ8w.js} +4 -4
- package/core/built/admin/assets/activitypub/{site-BFGj8HuQ.js → site-D1ZYV3AT.js} +2 -2
- package/core/built/admin/assets/activitypub/{step-1-CaoPg0a5.js → step-1-9IHheE78.js} +4 -4
- package/core/built/admin/assets/activitypub/{step-3-5k0TkyC9.js → step-3-DBERnfa-.js} +3 -3
- package/core/built/admin/assets/activitypub/{tabs-BV4JU7Vg.js → tabs-CvkwStij.js} +4 -4
- package/core/built/admin/assets/activitypub/{topic-filter-71sX5nfN.js → topic-filter-CS0Jsf_4.js} +2 -2
- package/core/built/admin/assets/activitypub/{use-activity-pub-queries-B3srUuKt.js → use-activity-pub-queries-CGgtQrxL.js} +2 -2
- package/core/built/admin/assets/activitypub/{use-reply-chain-data-Cl6bGNy7.js → use-reply-chain-data-CLdUpx1t.js} +4 -4
- package/core/built/admin/assets/activitypub/{users-EcSPpds5.js → users-CyoMXlwX.js} +11 -3
- package/core/built/admin/assets/{add-integration-modal-DBFzeAUT.js → add-integration-modal-BVg-ec8i.js} +1 -1
- package/core/built/admin/assets/{add-newsletter-modal-CYJe-dTC.js → add-newsletter-modal-Du6th521.js} +1 -1
- package/core/built/admin/assets/{add-recommendation-modal-CgwGKH-_.js → add-recommendation-modal-BiCJ0-q5.js} +1 -1
- package/core/built/admin/assets/{analytics-CcXT_WqF.js → analytics-CQJzDDdJ.js} +1 -1
- package/core/built/admin/assets/{announcement-bar-modal-Ldcg-PB-.js → announcement-bar-modal-CskyZLzT.js} +1 -1
- package/core/built/admin/assets/{api-BLteQZ7W.js → api-BFocMbDg.js} +2 -2
- package/core/built/admin/assets/{api-keys-CoeerpwI.js → api-keys-CqKJHuAV.js} +1 -1
- package/core/built/admin/assets/{audience-Deg9lzfk.js → audience-Dn7LuH-r.js} +1 -1
- package/core/built/admin/assets/{auth-layout-CRaMX7g6.js → auth-layout-VLhXgXn5.js} +1 -1
- package/core/built/admin/assets/{automated-emails-WcjjRkRV.js → automated-emails-D5cFl1wr.js} +1 -1
- package/core/built/admin/assets/{automations-CbIdX0rd.js → automations-BpweKEcq.js} +1 -1
- package/core/built/admin/assets/automations-DBTYwNTJ.js +1 -0
- package/core/built/admin/assets/{bluesky-sharing-BJMNRwnU.js → bluesky-sharing-CrfpFHQ9.js} +1 -1
- package/core/built/admin/assets/{chart-helpers-D-3N170i.js → chart-helpers-BSuFLd6-.js} +1 -1
- package/core/built/admin/assets/{chunk.524.94ab17e1199b4c3f4615.js → chunk.524.e24d68bc6253b274b726.js} +4 -4
- package/core/built/admin/assets/{chunk.582.6f09b19a007e2805824e.js → chunk.582.47834285406dfa1abb29.js} +6 -6
- package/core/built/admin/assets/{chunk.476.53b5794a67d68111e0bf.js → chunk.837.e1454abc054e1fba1fe5.js} +5 -5
- package/core/built/admin/assets/{code-editor-view-DhTwpxxv.js → code-editor-view-BrGe3eS4.js} +1 -1
- package/core/built/admin/assets/{code-editor-view-C7wXGiuv.js → code-editor-view-CY6JCXNv.js} +1 -1
- package/core/built/admin/assets/{color-picker-field-Cjz2zY11.js → color-picker-field-DGe9HH6W.js} +1 -1
- package/core/built/admin/assets/{comments-DY-VLVxh.js → comments-iRbF6XiJ.js} +1 -1
- package/core/built/admin/assets/{config-B3HuCo0y.js → config-CRAZ4Civ.js} +1 -1
- package/core/built/admin/assets/{content-api-modal-2jt-jWiM.js → content-api-modal-B-XRbzeW.js} +1 -1
- package/core/built/admin/assets/{content-formatters-VqvNA7Pv.js → content-formatters-C08JJbUJ.js} +2 -2
- package/core/built/admin/assets/{context-menu-BWbk9nd5.js → context-menu-BxGXsgQO.js} +1 -1
- package/core/built/admin/assets/{custom-field-type-option-CIatEwIN.js → custom-field-type-option-C8Nt4X6x.js} +1 -1
- package/core/built/admin/assets/{custom-integration-modal-Cw0w2Tb3.js → custom-integration-modal-DzI16qjN.js} +1 -1
- package/core/built/admin/assets/{date-range-select-O-uA-BRV.js → date-range-select-BZqsYsGx.js} +1 -1
- package/core/built/admin/assets/{design-and-theme-modal-BLjNHRYN.js → design-and-theme-modal-1ty_KEkM.js} +2 -2
- package/core/built/admin/assets/{dist-DJhCsld6.js → dist-B4jqKAG5.js} +1 -1
- package/core/built/admin/assets/{dist-5Ol0DC_t.js → dist-Bmo2j_KH.js} +1 -1
- package/core/built/admin/assets/{dist-p52UDdoR.js → dist-C-_aTlvJ.js} +1 -1
- package/core/built/admin/assets/{dist-CcN1a5SB.js → dist-CWHIoxYn.js} +1 -1
- package/core/built/admin/assets/{dist-BcfsaiCV.js → dist-Cp2M5TzT.js} +1 -1
- package/core/built/admin/assets/{dist-kjTYGivb.js → dist-DQAAPbtg.js} +1 -1
- package/core/built/admin/assets/{dist-IMek6Acg.js → dist-DyO279y5.js} +1 -1
- package/core/built/admin/assets/{domain-DaPAeOFF.js → domain-DiFl_Z1A.js} +1 -1
- package/core/built/admin/assets/{edit-profile-BKO5qonH.js → edit-profile-PrLbJV3P.js} +1 -1
- package/core/built/admin/assets/editor-BBJDJL8U.js +8 -0
- package/core/built/admin/assets/{editor-screen-l4BUPE2o.js → editor-screen-nblrp808.js} +3 -3
- package/core/built/admin/assets/{email-previews-B1Ska3CF.js → email-previews-BKJxrW29.js} +1 -1
- package/core/built/admin/assets/{email-sending-status-provider-Bjn5KTy2.js → email-sending-status-provider-DB9ylBWk.js} +1 -1
- package/core/built/admin/assets/{emails-qxNbhO6X.js → emails-8YiSmC2X.js} +1 -1
- package/core/built/admin/assets/{embed-signup-form-modal-DSgds6Js.js → embed-signup-form-modal-CZmbYKfd.js} +1 -1
- package/core/built/admin/assets/{error-boundary-CJODV8Rg.js → error-boundary-DrxPFFO-.js} +1 -1
- package/core/built/admin/assets/{esm-CNLDtBMc.js → esm-_iCUCvbN.js} +1 -1
- package/core/built/admin/assets/{explore-C6V_JpUZ.js → explore-C69LSRVg.js} +1 -1
- package/core/built/admin/assets/{feed-B_TyFn9E.js → feed-B_0m7WhZ.js} +1 -1
- package/core/built/admin/assets/{feedback-gKOzmiGt.js → feedback-DW1DXdzT.js} +1 -1
- package/core/built/admin/assets/{filters-Bf6E1qAP.js → filters-Dr3vMnjZ.js} +1 -1
- package/core/built/admin/assets/{filters-DhsiEotO.js → filters-M2sRWPoF.js} +1 -1
- package/core/built/admin/assets/{first-promoter-modal-LmTXHr-z.js → first-promoter-modal-D43R2zhI.js} +1 -1
- package/core/built/admin/assets/{focus-koenig-editor-on-bottom-click-BAcLNJti.js → focus-koenig-editor-on-bottom-click-BzS8jiJf.js} +1 -1
- package/core/built/admin/assets/fullscreen-close-button-CWSp-MsK.js +1 -0
- package/core/built/admin/assets/{ghost-63bc7da4336844000c523a5a0819531e.js → ghost-ffde29676517b768b1aef5ec8d492418.js} +93 -96
- package/core/built/admin/assets/{gift-link-modal-uZtwRHzl.js → gift-link-modal-CNE7dfPD.js} +1 -1
- package/core/built/admin/assets/{gift-links-HVqaXear.js → gift-links-lQ7H_D8Z.js} +1 -1
- package/core/built/admin/assets/global-search-modal-DzRM3aON.js +1 -0
- package/core/built/admin/assets/{growth-BlPW4DHj.js → growth-CAi_5HIf.js} +1 -1
- package/core/built/admin/assets/{growth-CflU3vR0.js → growth-CMHbTWKE.js} +1 -1
- package/core/built/admin/assets/history-modal-BIz13vhr.js +1 -0
- package/core/built/admin/assets/{hooks-C-BDQ6Hs.js → hooks-0r7qxGEo.js} +2 -2
- package/core/built/admin/assets/{html-field-D6G0G1d_.js → html-field-vTe2q1be.js} +1 -1
- package/core/built/admin/assets/{iframe-buffering-DW-Axhvc.js → iframe-buffering-cu-yK7NA.js} +1 -1
- package/core/built/admin/assets/{images-BSUWxquu.js → images-DTZtaIAr.js} +1 -1
- package/core/built/admin/assets/{inbox-BkU3sg13.js → inbox-BHfpmmzn.js} +1 -1
- package/core/built/admin/assets/{index-z8-nEVdb.css → index-B02LJqc8.css} +1 -1
- package/core/built/admin/assets/{index-CuDhPtkB.js → index-RuRs_qGS.js} +3 -3
- package/core/built/admin/assets/{index.es-H6HOI4DG.js → index.es-rdb1skp_.js} +1 -1
- package/core/built/admin/assets/{integrations-BRo5DwA5.js → integrations-Deg9-u8X.js} +1 -1
- package/core/built/admin/assets/{invite-user-modal-CydBOG99.js → invite-user-modal-C4DeNcoO.js} +1 -1
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +10483 -10455
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +131 -131
- package/core/built/admin/assets/{koenig-lexical-CG_eb4dw.js → koenig-lexical-BhXu1__-.js} +96 -96
- package/core/built/admin/assets/{kpi-helpers-CyRx_yD6.js → kpi-helpers-H-Hl6h7h.js} +1 -1
- package/core/built/admin/assets/{kpi-tabs-V1_Nu9sh.js → kpi-tabs-k4A9-imn.js} +1 -1
- package/core/built/admin/assets/{label-picker-GTIIlBQ6.js → label-picker-DaivLdMw.js} +1 -1
- package/core/built/admin/assets/{labels-BVGLgdAj.js → labels-BCj6D_nl.js} +1 -1
- package/core/built/admin/assets/{layout-CG58T6P7.js → layout-_g2xhB0L.js} +1 -1
- package/core/built/admin/assets/{lib-Dkic19kH.js → lib-Bre6UXdJ.js} +2 -2
- package/core/built/admin/assets/{links-jD8AkkIu.js → links-C8RnEJVJ.js} +1 -1
- package/core/built/admin/assets/{member-activity-JggQ0VNQ.js → member-activity-DNJPUW5X.js} +1 -1
- package/core/built/admin/assets/{member-activity-feed-BBeCZmLM.js → member-activity-feed-CvKEetHS.js} +1 -1
- package/core/built/admin/assets/{member-custom-fields-DPsyP1Hq.js → member-custom-fields-CBdiwPD9.js} +1 -1
- package/core/built/admin/assets/{member-detail-D7qugPN7.js → member-detail-BIoyIIsl.js} +1 -1
- package/core/built/admin/assets/{members-Cj_BUZUL.js → members-CH6KeXQc.js} +1 -1
- package/core/built/admin/assets/{members-alDWPKDd.js → members-Dd9RczFK.js} +1 -1
- package/core/built/admin/assets/{migrate-2uKKakld.js → migrate-CVQIWsXO.js} +1 -1
- package/core/built/admin/assets/{moderation-pCUKsxbF.js → moderation-OtjZoNOc.js} +1 -1
- package/core/built/admin/assets/{navigation-modal-D0FBI5-m.js → navigation-modal-Bl02NjtO.js} +1 -1
- package/core/built/admin/assets/{newsletter-4urgCSR_.js → newsletter-BXyT7o69.js} +1 -1
- package/core/built/admin/assets/{newsletter-detail-modal-Chh1jvjH.js → newsletter-detail-modal-DxuKNu_1.js} +1 -1
- package/core/built/admin/assets/{newsletter-emails-CzVCiC2K.js → newsletter-emails-DicgmnMU.js} +1 -1
- package/core/built/admin/assets/{newsletters-eRQTtOBH.js → newsletters-By25FmUs.js} +1 -1
- package/core/built/admin/assets/{newsletters-BMEgeL2l.js → newsletters-CACa0VCx.js} +1 -1
- package/core/built/admin/assets/{note-dqog4Bua.js → note-CF4LrSmR.js} +1 -1
- package/core/built/admin/assets/{notifications-rcEVv-gx.js → notifications-BmHJmqIJ.js} +1 -1
- package/core/built/admin/assets/{offers-hV7zBtsI.js → offers-Df9Jzay5.js} +1 -1
- package/core/built/admin/assets/{offers-container-modal-DgZgkMsA.js → offers-container-modal-R0HRwugm.js} +1 -1
- package/core/built/admin/assets/{onboarding-4TxbZb90.js → onboarding-Bpo1yTzA.js} +1 -1
- package/core/built/admin/assets/{onboarding-SGscZLIc.js → onboarding-bMq3R8jB.js} +1 -1
- package/core/built/admin/assets/{onboarding-route-B3mN9l7E.js → onboarding-route-flf5YWti.js} +1 -1
- package/core/built/admin/assets/{overview-D4EanvDG.js → overview-C9WvmmOC.js} +1 -1
- package/core/built/admin/assets/{overview-CCwbHet8.js → overview-CDkHdIJM.js} +1 -1
- package/core/built/admin/assets/{page-header-Baymg6VR.js → page-header-CxTb0je5.js} +1 -1
- package/core/built/admin/assets/{pagemenu--UL0gW1x.js → pagemenu-DXr_U43J.js} +1 -1
- package/core/built/admin/assets/{pages-Cbq1M2kG.js → pages-Bm-zFXyP.js} +1 -1
- package/core/built/admin/assets/pages-route-LCw94aIO.js +1 -0
- package/core/built/admin/assets/{pintura-modal-D6HGlnJa.js → pintura-modal-B5sIPWcj.js} +1 -1
- package/core/built/admin/assets/{portal-modal-Bhqsevn_.js → portal-modal-DjOO3Unv.js} +1 -1
- package/core/built/admin/assets/{post-analytics-header-DX9fETdi.js → post-analytics-header-D2anehCj.js} +1 -1
- package/core/built/admin/assets/{post-analytics-provider-U7RtOle1.js → post-analytics-provider-BLQvV5C0.js} +1 -1
- package/core/built/admin/assets/{post-analytics-BJWDtC3a.js → post-analytics-wdu9gq8u.js} +1 -1
- package/core/built/admin/assets/{post-debug-D1b8Amju.js → post-debug-BnABXMfD.js} +1 -1
- package/core/built/admin/assets/{posts-Lg-p_ZHg.js → posts-CftunZgV.js} +1 -1
- package/core/built/admin/assets/{posts-list-screen-CmsW18e_.js → posts-list-screen-CwXmvY68.js} +2 -2
- package/core/built/admin/assets/posts-route-J74fMhCe.js +1 -0
- package/core/built/admin/assets/{preferences-CMlDfHfa.js → preferences-DFhf_pJF.js} +1 -1
- package/core/built/admin/assets/{preview-modal-D8om_cQv.js → preview-modal-BrZza3R_.js} +1 -1
- package/core/built/admin/assets/{profile-yFsIoZom.js → profile-CS4fP60N.js} +1 -1
- package/core/built/admin/assets/{radio-group-DwgLjwWB.js → radio-group-Xfp-frJz.js} +1 -1
- package/core/built/admin/assets/{referrers-DzP5QoXL.js → referrers-C8LJlrBm.js} +1 -1
- package/core/built/admin/assets/{reload-CZ-KP3IQ.js → reload-D0fnKoCm.js} +1 -1
- package/core/built/admin/assets/{reset-CCAa8v35.js → reset-CB-ATcLE.js} +1 -1
- package/core/built/admin/assets/{restore-screen-Ba0i2Y1K.js → restore-screen-EaZf4V8j.js} +1 -1
- package/core/built/admin/assets/{routes-DrnhmKTT.js → routes-DKfpFMfF.js} +2 -2
- package/core/built/admin/assets/search-index-sM3OjEUl.js +1 -0
- package/core/built/admin/assets/settings-BWzod-5X.js +4 -0
- package/core/built/admin/assets/{settings-lqWkfSmT.js → settings-DvgngbFt.js} +1 -1
- package/core/built/admin/assets/{setup-BopRPuaM.js → setup-CdHAchG5.js} +1 -1
- package/core/built/admin/assets/{signin-e8NgsU9M.js → signin-CUEEme88.js} +1 -1
- package/core/built/admin/assets/{signin-verify-CHkFJevb.js → signin-verify-CYRphSrj.js} +1 -1
- package/core/built/admin/assets/{signout-DRvQZuWa.js → signout-kLCJau9G.js} +1 -1
- package/core/built/admin/assets/{signup-DQHi7jZz.js → signup-J2Vjq7Gt.js} +1 -1
- package/core/built/admin/assets/{slack-modal-BuGESzgn.js → slack-modal-DKamr_94.js} +1 -1
- package/core/built/admin/assets/source-icon-DqBCPDIK.js +1 -0
- package/core/built/admin/assets/{stats-CHOikfoP.js → stats-BL_FJ7Ns.js} +1 -1
- package/core/built/admin/assets/{stats-view-DQZzKeEz.js → stats-view-BwbdkdOQ.js} +1 -1
- package/core/built/admin/assets/{step-1-C8XwHcKt.js → step-1-CLYhNmG9.js} +1 -1
- package/core/built/admin/assets/{step-2-BXaXAdwY.js → step-2-B2ujMlpa.js} +1 -1
- package/core/built/admin/assets/{step-3-DytbqpJi.js → step-3-DoaTWqqj.js} +1 -1
- package/core/built/admin/assets/{stripe-connect-modal-BhudjSeO.js → stripe-connect-modal-DytxH9DJ.js} +1 -1
- package/core/built/admin/assets/{tabs-CDb_TceL.js → tabs-fHs3A8WC.js} +1 -1
- package/core/built/admin/assets/{tag-detail-V72Zlro8.js → tag-detail-BcO46Ids.js} +2 -2
- package/core/built/admin/assets/{tag-picker-M3fC-yOF.js → tag-picker-Ct6NeqWK.js} +1 -1
- package/core/built/admin/assets/{tags-CXqE2A2T.js → tags-C1h3wCpW.js} +1 -1
- package/core/built/admin/assets/{tags-ZhqbPPc6.js → tags-OlmRJ6Ps.js} +1 -1
- package/core/built/admin/assets/{testimonials-modal-Cn8gvIFA.js → testimonials-modal-DMsQzSCB.js} +1 -1
- package/core/built/admin/assets/{themes-pOHHvnkt.js → themes-CPJSMo_W.js} +1 -1
- package/core/built/admin/assets/{tier-detail-modal-CeDla1OO.js → tier-detail-modal-DssSmhkF.js} +1 -1
- package/core/built/admin/assets/{tiers-D7aGzwZ-.js → tiers-DlvS8D6_.js} +1 -1
- package/core/built/admin/assets/{toggle-group-D7ly9OGa.js → toggle-group-BFWCtH3p.js} +1 -1
- package/core/built/admin/assets/{topic-filter-DQ3pr6AB.js → topic-filter-DAJ-DFPb.js} +1 -1
- package/core/built/admin/assets/{transistor-modal-4_Yx2vRo.js → transistor-modal-CtyFRsD0.js} +1 -1
- package/core/built/admin/assets/{unsplash-modal-Dm7R4k4k.js → unsplash-modal-DHnnTPmZ.js} +1 -1
- package/core/built/admin/assets/{use-activity-pub-queries-Gy9_locM.js → use-activity-pub-queries-BgR1USeY.js} +1 -1
- package/core/built/admin/assets/{use-analytics-data-oJ2o0pSS.js → use-analytics-data-Byf83QE-.js} +1 -1
- package/core/built/admin/assets/{use-availability-BqL7Dvn_.js → use-availability-CgZC6L_V.js} +1 -1
- package/core/built/admin/assets/{use-check-theme-limit-error-CxC9uKJz.js → use-check-theme-limit-error-DjprzKAN.js} +1 -1
- package/core/built/admin/assets/use-definitions-CJuXLSob.js +1 -0
- package/core/built/admin/assets/{use-email-sending-status-BFAg5TIb.js → use-email-sending-status-7kopNcza.js} +1 -1
- package/core/built/admin/assets/{use-filter-params-BOo9yZ5j.js → use-filter-params-DQ7uX-Zp.js} +1 -1
- package/core/built/admin/assets/{use-filterable-api-BnNLaS_r.js → use-filterable-api-CDrrxoGD.js} +1 -1
- package/core/built/admin/assets/{use-growth-stats-BS62uyyn.js → use-growth-stats-CCpnBgqS.js} +1 -1
- package/core/built/admin/assets/{use-host-limits-CNh4AAEs.js → use-host-limits-BZx7rzRI.js} +1 -1
- package/core/built/admin/assets/{use-limiter-DPKxoO1_.js → use-limiter-gF219BEg.js} +1 -1
- package/core/built/admin/assets/{use-pintura-config-CyVO62ZV.js → use-pintura-config-D-pmGkn2.js} +1 -1
- package/core/built/admin/assets/{use-pintura-editor-B5VBPbiO.js → use-pintura-editor-D_4zhlGE.js} +1 -1
- package/core/built/admin/assets/{use-post-referrers-nUPolSk7.js → use-post-referrers-Bc8eq4uy.js} +1 -1
- package/core/built/admin/assets/{use-post-top-links-DCH4eO11.js → use-post-top-links-DjltnQuI.js} +1 -1
- package/core/built/admin/assets/{use-reply-chain-data-UMzFbDvh.js → use-reply-chain-data-BiV3Cl0B.js} +1 -1
- package/core/built/admin/assets/use-sending-eta-gE-2B0Zw.js +1 -0
- package/core/built/admin/assets/{use-setting-group-Bf3vY8br.js → use-setting-group-YP84CqrJ.js} +1 -1
- package/core/built/admin/assets/{use-staff-users-CcQzTHqH.js → use-staff-users-CnwfdSx1.js} +1 -1
- package/core/built/admin/assets/{use-tinybird-query-CPy5ipAP.js → use-tinybird-query-BYbl5gKY.js} +1 -1
- package/core/built/admin/assets/{use-tinybird-token-DVNHuhu_.js → use-tinybird-token-Dz37zvb7.js} +1 -1
- package/core/built/admin/assets/{use-unsaved-changes-guard-Bvcp_Rny.js → use-unsaved-changes-guard-BkCuSlvU.js} +1 -1
- package/core/built/admin/assets/{use-upgrade-route-Bsxw9ZLN.js → use-upgrade-route-C-V-Xfak.js} +1 -1
- package/core/built/admin/assets/{use-url-input-DPJ592FB.js → use-url-input-BeYKYMGh.js} +1 -1
- package/core/built/admin/assets/{user-detail-modal-6UY6GdNy.js → user-detail-modal-CSoj7vOC.js} +1 -1
- package/core/built/admin/assets/{users-CzgQKbfO.js → users-CKJaAxEJ.js} +1 -1
- package/core/built/admin/assets/{view-site-DA_-zfGq.js → view-site-SpMx4P7G.js} +1 -1
- package/core/built/admin/assets/{web-Cq5YiD2r.js → web-CZf-k4F0.js} +1 -1
- package/core/built/admin/assets/{web-DqFIrOxt.js → web-uAqgYPK9.js} +1 -1
- package/core/built/admin/assets/{zapier-modal-DgqU-OBB.js → zapier-modal-DENbgTyE.js} +1 -1
- package/core/built/admin/index.html +24 -24
- package/core/frontend/helpers/color_to_rgba.js +15 -13
- package/core/frontend/helpers/facebook_url.js +27 -15
- package/core/frontend/services/assets-minification/card-assets.js +1 -1
- package/core/frontend/services/helpers/register-ghost-helpers.js +2 -2
- package/core/server/api/endpoints/app-installations.js +36 -0
- package/core/server/api/endpoints/automations.js +1 -1
- package/core/server/api/endpoints/index.js +4 -0
- package/core/server/data/exporter/table-lists.js +2 -0
- package/core/server/data/migrations/versions/6.69/2026-10-07-06-20-43-add-app-installation-permissions.js +40 -0
- package/core/server/data/schema/fixtures/fixtures.json +21 -0
- package/core/server/data/schema/in-development.js +1 -1
- package/core/server/data/schema/schema.js +53 -0
- package/core/server/lib/after-commit.js +20 -0
- package/core/server/models/action.js +6 -0
- package/core/server/services/app-installations/actions.js +44 -0
- package/core/server/services/app-installations/codec.js +20 -0
- package/core/server/services/app-installations/index.js +42 -0
- package/core/server/services/app-installations/schema.js +52 -0
- package/core/server/services/app-installations/service.js +160 -0
- package/core/server/services/automations/README.md +3 -3
- package/core/server/services/automations/automations-api.js +1 -1
- package/core/server/services/comments/comments-service-emails.js +8 -6
- package/core/server/services/email-service/README.md +16 -0
- package/core/server/services/email-service/email-service-wrapper.js +1 -0
- package/core/server/services/email-service/email-service.js +94 -53
- package/core/server/services/email-service/sending-status-serializers.js +4 -1
- package/core/server/services/email-service/sending-status-service.js +13 -0
- package/core/server/services/email-service/sending-status.js +12 -1
- package/core/server/services/members/members-api/repositories/member-repository.js +5 -23
- package/core/server/services/posts/post-email-handler.js +61 -28
- package/core/server/services/posts/posts-service.js +27 -2
- package/core/server/services/service-inventory.yaml +93 -7
- package/core/server/services/stats/stats-service.js +1 -5
- package/core/server/services/tinybird-sync/index.js +0 -3
- package/core/server/services/tinybird-sync/tinybird-sync-service.js +2 -6
- package/core/server/web/api/endpoints/admin/routes.js +25 -12
- package/core/shared/labs.js +4 -6
- package/package.json +11 -10
- package/pnpm-lock.yaml +112 -100
- package/pnpm-workspace.yaml +14 -12
- package/components/ghost-storage-base-3.0.0.tgz +0 -0
- package/components/tryghost-adapter-base-cache-0.2.0.tgz +0 -0
- package/components/tryghost-checkout-0.0.1.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-transforms-1.3.4.tgz +0 -0
- package/components/tryghost-kg-html-to-lexical-1.4.0.tgz +0 -0
- package/components/tryghost-kg-markdown-html-renderer-7.2.6.tgz +0 -0
- package/components/tryghost-kg-utils-1.1.4.tgz +0 -0
- package/components/tryghost-metafield-csv-0.0.1.tgz +0 -0
- package/core/built/admin/assets/activitypub/layout-QTbY0t8e.js +0 -1
- package/core/built/admin/assets/automations-DVToctfJ.js +0 -1
- package/core/built/admin/assets/editor-CMXR0YvR.js +0 -8
- package/core/built/admin/assets/fullscreen-close-button-DXssPeDB.js +0 -1
- package/core/built/admin/assets/global-search-modal-C8LVJmiz.js +0 -1
- package/core/built/admin/assets/history-modal-parBtjpz.js +0 -1
- package/core/built/admin/assets/pages-route-CDk3V9rq.js +0 -1
- package/core/built/admin/assets/posts-route-CexHhklY.js +0 -1
- package/core/built/admin/assets/settings-xqakrAWa.js +0 -4
- package/core/built/admin/assets/source-icon-BjVangBd.js +0 -1
- package/core/built/admin/assets/use-definitions-DMVU3mTG.js +0 -1
- package/core/built/admin/assets/use-sending-eta-BsnKdQhv.js +0 -1
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.actingContext = actingContext;
|
|
7
|
+
exports.recordAppInstallationAction = recordAppInstallationAction;
|
|
8
|
+
const logging_1 = __importDefault(require("@tryghost/logging"));
|
|
9
|
+
/**
|
|
10
|
+
* Who is acting, read off an API frame's context.
|
|
11
|
+
*
|
|
12
|
+
* Only a staff user counts, whether signed in or using their own staff token.
|
|
13
|
+
* Integrations hold no permission to manage apps, so they never get this far.
|
|
14
|
+
*/
|
|
15
|
+
function actingContext(context) {
|
|
16
|
+
const frame = (context ?? {});
|
|
17
|
+
if (frame.user && !frame.integration) {
|
|
18
|
+
return { actor: { id: frame.user, type: 'user' } };
|
|
19
|
+
}
|
|
20
|
+
return { actor: null };
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Staff history is where "who installed this, and when" lives; the installation itself
|
|
24
|
+
* does not hold it. The write is best-effort: a failed action must never fail the install
|
|
25
|
+
* or uninstall that triggered it.
|
|
26
|
+
*/
|
|
27
|
+
async function recordAppInstallationAction({ Action, context, event, subject, details, }) {
|
|
28
|
+
if (!context.actor) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
await Action.add({
|
|
33
|
+
event,
|
|
34
|
+
resource_type: 'app_installation',
|
|
35
|
+
resource_id: subject,
|
|
36
|
+
actor_type: context.actor.type,
|
|
37
|
+
actor_id: context.actor.id,
|
|
38
|
+
context: details,
|
|
39
|
+
}, { autoRefresh: false });
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
logging_1.default.error(err);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AppInstallationRow = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const schema_1 = require("./schema");
|
|
6
|
+
/**
|
|
7
|
+
* An installation joined with the approved manifest it runs, as the read query returns it
|
|
8
|
+
* and as the Admin API returns it. The columns keep their names; decoding is what turns
|
|
9
|
+
* the dates, the status and the manifest text into what the rest of Ghost works with.
|
|
10
|
+
*/
|
|
11
|
+
exports.AppInstallationRow = zod_1.z.object({
|
|
12
|
+
...schema_1.DbAppInstallation.pick({
|
|
13
|
+
id: true,
|
|
14
|
+
app_id: true,
|
|
15
|
+
status: true,
|
|
16
|
+
created_at: true,
|
|
17
|
+
updated_at: true,
|
|
18
|
+
}).shape,
|
|
19
|
+
...schema_1.DbAppInstallationManifest.pick({ manifest_url: true, manifest: true }).shape,
|
|
20
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.service = exports.actingContext = void 0;
|
|
7
|
+
exports.init = init;
|
|
8
|
+
exports.isAvailable = isAvailable;
|
|
9
|
+
const config_1 = __importDefault(require("../../../shared/config"));
|
|
10
|
+
const url_utils_1 = __importDefault(require("../../../shared/url-utils"));
|
|
11
|
+
const in_development_1 = require("../../data/schema/in-development");
|
|
12
|
+
const actions_1 = require("./actions");
|
|
13
|
+
const service_1 = require("./service");
|
|
14
|
+
var actions_2 = require("./actions");
|
|
15
|
+
Object.defineProperty(exports, "actingContext", { enumerable: true, get: function () { return actions_2.actingContext; } });
|
|
16
|
+
function init() {
|
|
17
|
+
if (exports.service) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const { knex } = require('../../data/db');
|
|
21
|
+
const models = require('../../models');
|
|
22
|
+
const recordAction = (input) => (0, actions_1.recordAppInstallationAction)({ Action: models.Action, ...input });
|
|
23
|
+
exports.service = new service_1.AppInstallationsService({
|
|
24
|
+
knex,
|
|
25
|
+
recordAction,
|
|
26
|
+
// Read per install, not once at boot: both are config, which tests change.
|
|
27
|
+
getManifestRules: () => ({
|
|
28
|
+
ghostUrls: [url_utils_1.default.urlFor('home', true), url_utils_1.default.urlFor('admin', true)],
|
|
29
|
+
allowLocalhost: config_1.default.get('env') === 'development',
|
|
30
|
+
}),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Whether this database has the installations table.
|
|
35
|
+
*
|
|
36
|
+
* While the table is still in development it only exists in development and testing
|
|
37
|
+
* databases. Once it is finalised and has a migration it exists everywhere, and this is
|
|
38
|
+
* always true.
|
|
39
|
+
*/
|
|
40
|
+
function isAvailable() {
|
|
41
|
+
return !(0, in_development_1.isInDevelopmentTable)('app_installations') || (0, in_development_1.shouldCreateInDevelopmentTables)();
|
|
42
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DbAppInstallationManifest = exports.StoredManifest = exports.DbAppInstallation = exports.AppInstallationStatus = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const manifest_1 = require("@tryghost/app-contracts/manifest");
|
|
6
|
+
const boolean_1 = require("../../lib/db-types/boolean");
|
|
7
|
+
const date_1 = require("../../lib/db-types/date");
|
|
8
|
+
// Mirrors schema.js's `isIn` on the column, which is static config and cannot import this.
|
|
9
|
+
exports.AppInstallationStatus = zod_1.z.enum(['active', 'suspended', 'uninstalled']);
|
|
10
|
+
/** An installation as the table holds it. */
|
|
11
|
+
exports.DbAppInstallation = zod_1.z.object({
|
|
12
|
+
id: zod_1.z.string(),
|
|
13
|
+
app_id: zod_1.z.string(),
|
|
14
|
+
current_app_id: zod_1.z.string().nullable(),
|
|
15
|
+
status: exports.AppInstallationStatus,
|
|
16
|
+
manifest_id: zod_1.z.string(),
|
|
17
|
+
pending_manifest_id: zod_1.z.string().nullable(),
|
|
18
|
+
revision: zod_1.z.number().int().nonnegative(),
|
|
19
|
+
created_at: date_1.DbDate,
|
|
20
|
+
updated_at: date_1.DbDate.nullable(),
|
|
21
|
+
});
|
|
22
|
+
/**
|
|
23
|
+
* The manifest as the table holds it, JSON text, against the manifest itself. A codec
|
|
24
|
+
* rather than a parse on the way out, so the write stores what the read accepts: the
|
|
25
|
+
* digest is taken from this encoding, and the read decodes exactly it.
|
|
26
|
+
*/
|
|
27
|
+
exports.StoredManifest = zod_1.z.codec(zod_1.z.string(), manifest_1.AppManifestSchema, {
|
|
28
|
+
decode: (text, ctx) => {
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(text);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
ctx.issues.push({
|
|
34
|
+
code: 'custom',
|
|
35
|
+
message: 'The stored manifest is not JSON.',
|
|
36
|
+
input: text,
|
|
37
|
+
});
|
|
38
|
+
return zod_1.z.NEVER;
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
encode: (manifest) => JSON.stringify(manifest),
|
|
42
|
+
});
|
|
43
|
+
/** A manifest an installation has run or been asked to approve, as the table holds it. */
|
|
44
|
+
exports.DbAppInstallationManifest = zod_1.z.object({
|
|
45
|
+
id: zod_1.z.string(),
|
|
46
|
+
installation_id: zod_1.z.string(),
|
|
47
|
+
manifest_url: zod_1.z.string(),
|
|
48
|
+
manifest: exports.StoredManifest,
|
|
49
|
+
digest: zod_1.z.string(),
|
|
50
|
+
requires_approval: boolean_1.DbBoolean,
|
|
51
|
+
created_at: date_1.DbDate,
|
|
52
|
+
});
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.AppInstallationsService = void 0;
|
|
7
|
+
const node_crypto_1 = require("node:crypto");
|
|
8
|
+
const errors_1 = __importDefault(require("@tryghost/errors"));
|
|
9
|
+
const bson_objectid_1 = __importDefault(require("bson-objectid"));
|
|
10
|
+
const zod_1 = require("zod");
|
|
11
|
+
const manifest_1 = require("@tryghost/app-contracts/manifest");
|
|
12
|
+
const date_1 = require("../../lib/db-types/date");
|
|
13
|
+
const codec_1 = require("./codec");
|
|
14
|
+
const schema_1 = require("./schema");
|
|
15
|
+
const INSTALLATIONS = 'app_installations';
|
|
16
|
+
const MANIFESTS = 'app_installation_manifests';
|
|
17
|
+
/**
|
|
18
|
+
* What a publisher reviews and confirms. The same manifest always serialises the same way,
|
|
19
|
+
* as encoding builds it in the schema's key order.
|
|
20
|
+
*/
|
|
21
|
+
function digestOf(serialisedManifest) {
|
|
22
|
+
return (0, node_crypto_1.createHash)('sha256').update(serialisedManifest).digest('hex');
|
|
23
|
+
}
|
|
24
|
+
function isUniqueViolation(err) {
|
|
25
|
+
const code = err?.code;
|
|
26
|
+
return (code === 'ER_DUP_ENTRY' || (typeof code === 'string' && code.startsWith('SQLITE_CONSTRAINT')));
|
|
27
|
+
}
|
|
28
|
+
class AppInstallationsService {
|
|
29
|
+
knex;
|
|
30
|
+
recordAction;
|
|
31
|
+
getManifestRules;
|
|
32
|
+
constructor({ knex, recordAction, getManifestRules, }) {
|
|
33
|
+
this.knex = knex;
|
|
34
|
+
this.recordAction = recordAction;
|
|
35
|
+
this.getManifestRules = getManifestRules;
|
|
36
|
+
}
|
|
37
|
+
/** Installations with the approved manifest each one runs. */
|
|
38
|
+
withApprovedManifest() {
|
|
39
|
+
return this.knex(`${INSTALLATIONS} as installation`)
|
|
40
|
+
.join(`${MANIFESTS} as approved`, 'approved.id', 'installation.manifest_id')
|
|
41
|
+
.select('installation.id', 'installation.app_id', 'installation.status', 'approved.manifest_url', 'approved.manifest', 'installation.created_at', 'installation.updated_at');
|
|
42
|
+
}
|
|
43
|
+
/** The apps installed on this site now, suspended ones included. Ended ones are not listed. */
|
|
44
|
+
async browse() {
|
|
45
|
+
const rows = await this.withApprovedManifest()
|
|
46
|
+
.whereNot('installation.status', 'uninstalled')
|
|
47
|
+
.orderBy('installation.created_at', 'asc')
|
|
48
|
+
.orderBy('installation.id', 'asc');
|
|
49
|
+
return rows.map((row) => zod_1.z.decode(codec_1.AppInstallationRow, row));
|
|
50
|
+
}
|
|
51
|
+
/** One installation by ID, whether it has ended or not. */
|
|
52
|
+
async read(id) {
|
|
53
|
+
const [row] = await this.withApprovedManifest().where('installation.id', id);
|
|
54
|
+
if (!row) {
|
|
55
|
+
throw new errors_1.default.NotFoundError({ message: 'App installation not found.' });
|
|
56
|
+
}
|
|
57
|
+
return zod_1.z.decode(codec_1.AppInstallationRow, row);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Installs an app from a manifest Ghost itself fetched from `manifestUrl`.
|
|
61
|
+
*
|
|
62
|
+
* Every install is a new installation with a new ID, including a reinstall: an ended
|
|
63
|
+
* installation is never brought back. The unique `current_app_id` is what refuses a
|
|
64
|
+
* second installation of the same app, so two confirmations racing each other end with
|
|
65
|
+
* exactly one installed and the other told so.
|
|
66
|
+
*/
|
|
67
|
+
async install(context, { manifestUrl, manifest }) {
|
|
68
|
+
const parsed = (0, manifest_1.parseManifest)(manifest, { manifestUrl, ...this.getManifestRules() });
|
|
69
|
+
if (!parsed.success) {
|
|
70
|
+
throw new errors_1.default.ValidationError({
|
|
71
|
+
message: 'The app manifest is not valid.',
|
|
72
|
+
context: parsed.errors
|
|
73
|
+
.map(({ path, message }) => (path ? `${path}: ${message}` : message))
|
|
74
|
+
.join('; '),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const id = new bson_objectid_1.default().toHexString();
|
|
78
|
+
const manifestId = new bson_objectid_1.default().toHexString();
|
|
79
|
+
const serialisedManifest = zod_1.z.encode(schema_1.StoredManifest, parsed.manifest);
|
|
80
|
+
const now = (0, date_1.toDatabaseDate)(new Date());
|
|
81
|
+
// What the read query would return for this row, so the result is decoded from what
|
|
82
|
+
// was written rather than read back: the same codec reads every row, which proves the
|
|
83
|
+
// write readable without a second query.
|
|
84
|
+
const written = {
|
|
85
|
+
id,
|
|
86
|
+
app_id: parsed.manifest.id,
|
|
87
|
+
status: 'active',
|
|
88
|
+
manifest_url: parsed.manifestUrl,
|
|
89
|
+
manifest: serialisedManifest,
|
|
90
|
+
created_at: now,
|
|
91
|
+
updated_at: now,
|
|
92
|
+
};
|
|
93
|
+
try {
|
|
94
|
+
await this.knex.transaction(async (trx) => {
|
|
95
|
+
await trx(INSTALLATIONS).insert({
|
|
96
|
+
id,
|
|
97
|
+
app_id: written.app_id,
|
|
98
|
+
current_app_id: written.app_id,
|
|
99
|
+
status: written.status,
|
|
100
|
+
manifest_id: manifestId,
|
|
101
|
+
created_at: now,
|
|
102
|
+
updated_at: now,
|
|
103
|
+
});
|
|
104
|
+
await trx(MANIFESTS).insert({
|
|
105
|
+
id: manifestId,
|
|
106
|
+
installation_id: id,
|
|
107
|
+
// As checked, not as given: the contract keeps it within the column's width.
|
|
108
|
+
manifest_url: written.manifest_url,
|
|
109
|
+
manifest: serialisedManifest,
|
|
110
|
+
digest: digestOf(serialisedManifest),
|
|
111
|
+
requires_approval: false,
|
|
112
|
+
created_at: now,
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
if (isUniqueViolation(err)) {
|
|
118
|
+
throw new errors_1.default.ConflictError({
|
|
119
|
+
message: `The app ${parsed.manifest.id} is already installed.`,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
throw err;
|
|
123
|
+
}
|
|
124
|
+
await this.recordAction({
|
|
125
|
+
context,
|
|
126
|
+
event: 'installed',
|
|
127
|
+
subject: id,
|
|
128
|
+
details: { primary_name: parsed.manifest.name, app_id: parsed.manifest.id },
|
|
129
|
+
});
|
|
130
|
+
return zod_1.z.decode(codec_1.AppInstallationRow, written);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Ends an installation. The row stays, so the site keeps a record of what was installed.
|
|
134
|
+
*
|
|
135
|
+
* Uninstalling one that has already ended does nothing and records nothing, so a
|
|
136
|
+
* repeated request cannot add a second uninstall to the history.
|
|
137
|
+
*/
|
|
138
|
+
async uninstall(context, id) {
|
|
139
|
+
const installation = await this.read(id);
|
|
140
|
+
const ended = await this.knex(INSTALLATIONS)
|
|
141
|
+
.where({ id })
|
|
142
|
+
.whereNot('status', 'uninstalled')
|
|
143
|
+
.update({
|
|
144
|
+
current_app_id: null,
|
|
145
|
+
status: 'uninstalled',
|
|
146
|
+
revision: this.knex.raw('?? + 1', ['revision']),
|
|
147
|
+
updated_at: (0, date_1.toDatabaseDate)(new Date()),
|
|
148
|
+
});
|
|
149
|
+
if (ended === 0) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
await this.recordAction({
|
|
153
|
+
context,
|
|
154
|
+
event: 'uninstalled',
|
|
155
|
+
subject: id,
|
|
156
|
+
details: { primary_name: installation.manifest.name, app_id: installation.app_id },
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
exports.AppInstallationsService = AppInstallationsService;
|
|
@@ -191,9 +191,9 @@ follow the same cursor ordering as other candidates.
|
|
|
191
191
|
|
|
192
192
|
## Availability
|
|
193
193
|
|
|
194
|
-
The performance endpoint requires
|
|
195
|
-
|
|
196
|
-
|
|
194
|
+
The performance endpoint requires Tinybird configuration. Missing
|
|
195
|
+
configuration/token, unavailable pipe, or invalid response produces an error.
|
|
196
|
+
The automation list retains its MySQL fallback for those cases; a successful
|
|
197
197
|
empty Tinybird response still returns zero counts rather than falling back.
|
|
198
198
|
|
|
199
199
|
The `automationRunAnalytics` flag controls presentation. See the
|
|
@@ -136,7 +136,7 @@ const repository = (0, database_automations_repository_1.createDatabaseAutomatio
|
|
|
136
136
|
fakeWaitHoursMultiplier: (0, fake_wait_hours_multiplier_1.parseFakeWaitHoursMultiplier)(config.get('automations:fakeWaitHoursMultiplier')),
|
|
137
137
|
});
|
|
138
138
|
function getTinybirdClient() {
|
|
139
|
-
if (!
|
|
139
|
+
if (!config.get('tinybird:stats')) {
|
|
140
140
|
return null;
|
|
141
141
|
}
|
|
142
142
|
try {
|
|
@@ -191,12 +191,14 @@ class CommentsServiceEmails {
|
|
|
191
191
|
* @param {*} reporter The member object who reported this comment
|
|
192
192
|
*/
|
|
193
193
|
async notifyReport(comment, reporter) {
|
|
194
|
-
const post = await
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
194
|
+
const [post, member, owner] = await Promise.all([
|
|
195
|
+
this.models.Post.findOne(
|
|
196
|
+
{ id: comment.get('post_id') },
|
|
197
|
+
{ withRelated: ['tags', 'authors'] },
|
|
198
|
+
),
|
|
199
|
+
this.models.Member.findOne({ id: comment.get('member_id') }),
|
|
200
|
+
this.models.User.getOwnerUser(),
|
|
201
|
+
]);
|
|
200
202
|
|
|
201
203
|
// For now we only send the report to the owner
|
|
202
204
|
const to = owner.get('email');
|
|
@@ -345,6 +345,22 @@ attempts, so a retried email reports submitting with frozen progress while it
|
|
|
345
345
|
waits for its job, and `failed_during` is the same derivation applied to a
|
|
346
346
|
failed email.
|
|
347
347
|
|
|
348
|
+
Failed status includes `retryable`, derived from the current email and batch
|
|
349
|
+
state. A failed email is retryable unless any batch is still `submitting`: the
|
|
350
|
+
provider may have accepted that batch even though Ghost did not record the
|
|
351
|
+
outcome. `EmailService.retryEmail` re-reads the same eligibility before queuing a
|
|
352
|
+
retry, for both the retry endpoint and post saves. Admin requires an explicit
|
|
353
|
+
`retryable: true`; older Core responses without the field hide Retry without
|
|
354
|
+
requiring access to the batch-browsing endpoint.
|
|
355
|
+
|
|
356
|
+
Orphaned `submitting` batches have no automatic recovery path: an operator must
|
|
357
|
+
inspect the batch's Mailgun message ID, provider delivery records, and Ghost's
|
|
358
|
+
submission logs to reconcile its outcome before a retry can be considered safe.
|
|
359
|
+
|
|
360
|
+
Eligibility never blocks a post save. Republishing a post whose failed email is
|
|
361
|
+
not retryable saves the post and leaves the email failed, without queuing a
|
|
362
|
+
retry; the retry endpoint rejects the same email with a 400.
|
|
363
|
+
|
|
348
364
|
The rough ETA measures recent recipient throughput, including work completed by
|
|
349
365
|
concurrent workers. It stays `null` until enough timing samples are available in
|
|
350
366
|
the current phase and attempt, so short sends may finish without showing an ETA.
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* @property {object} newsletter
|
|
12
12
|
* @property {string} emailRecipientFilter
|
|
13
13
|
* @property {number} emailCount
|
|
14
|
+
* @property {number} [csdEmailCount] - How many to send from the warming domain, when domain warming is enabled
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
17
|
const BatchSendingService = require('./batch-sending-service');
|
|
@@ -27,6 +28,8 @@ const messages = {
|
|
|
27
28
|
emailSendingDisabled: `Email sending is temporarily disabled because your account is currently in review. You should have an email about this from us already, but you can also reach us any time at support@ghost.org`,
|
|
28
29
|
retryEmailStatusError: 'Can only retry emails for published posts',
|
|
29
30
|
retryEmailNotFailed: 'Only failed emails can be retried',
|
|
31
|
+
retryEmailUnknownOutcome: 'Cannot retry email because the delivery outcome is unknown',
|
|
32
|
+
postChangedWhilePublishing: 'The post was changed while it was being published, please try again',
|
|
30
33
|
};
|
|
31
34
|
|
|
32
35
|
// Resume scanner won't pick up `pending` or `submitting` rows older than this. Rows beyond
|
|
@@ -38,9 +41,13 @@ const DEFAULT_RESUME_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
|
38
41
|
// Non-terminal email statuses, both of which the boot scanner recovers.
|
|
39
42
|
const RESUMABLE_EMAIL_STATUSES = ['pending', 'submitting'];
|
|
40
43
|
|
|
44
|
+
const RETRY_UNKNOWN_OUTCOME_CODE = 'EMAIL_RETRY_UNKNOWN_OUTCOME';
|
|
45
|
+
const RETRY_NOT_FAILED_CODE = 'BULK_EMAIL_RETRY_NOT_FAILED';
|
|
46
|
+
|
|
41
47
|
class EmailService {
|
|
42
48
|
#batchSendingService;
|
|
43
49
|
#sendingService;
|
|
50
|
+
#sendingStatusService;
|
|
44
51
|
#models;
|
|
45
52
|
#settingsCache;
|
|
46
53
|
#emailRenderer;
|
|
@@ -57,6 +64,7 @@ class EmailService {
|
|
|
57
64
|
* @param {object} dependencies
|
|
58
65
|
* @param {BatchSendingService} dependencies.batchSendingService
|
|
59
66
|
* @param {SendingService} dependencies.sendingService
|
|
67
|
+
* @param {import('./sending-status-service').SendingStatusService} dependencies.sendingStatusService
|
|
60
68
|
* @param {object} dependencies.models
|
|
61
69
|
* @param {object} dependencies.models.Email
|
|
62
70
|
* @param {object} [dependencies.models.EmailBatch] - Required for resumeInterruptedSends breadcrumbs
|
|
@@ -73,6 +81,7 @@ class EmailService {
|
|
|
73
81
|
constructor({
|
|
74
82
|
batchSendingService,
|
|
75
83
|
sendingService,
|
|
84
|
+
sendingStatusService,
|
|
76
85
|
models,
|
|
77
86
|
settingsCache,
|
|
78
87
|
emailRenderer,
|
|
@@ -92,6 +101,7 @@ class EmailService {
|
|
|
92
101
|
this.#limitService = limitService;
|
|
93
102
|
this.#membersRepository = membersRepository;
|
|
94
103
|
this.#sendingService = sendingService;
|
|
104
|
+
this.#sendingStatusService = sendingStatusService;
|
|
95
105
|
this.#verificationTrigger = verificationTrigger;
|
|
96
106
|
this.#emailAnalyticsJobs = emailAnalyticsJobs;
|
|
97
107
|
this.#domainWarmingService = domainWarmingService;
|
|
@@ -132,11 +142,9 @@ class EmailService {
|
|
|
132
142
|
*
|
|
133
143
|
* @param {object} newsletter - The newsletter model to send to
|
|
134
144
|
* @param {string} emailRecipientFilter - The recipient filter for the email
|
|
135
|
-
* @
|
|
136
|
-
* @param {number} [options.emailCount] - A previously counted audience to revalidate without recounting
|
|
137
|
-
* @returns {Promise<{emailCount: number}>} The email count if checks pass, throws if email cannot be sent
|
|
145
|
+
* @returns {Promise<{emailCount: number, csdEmailCount?: number}>} The recipient counts if checks pass, throws if email cannot be sent
|
|
138
146
|
*/
|
|
139
|
-
async checkCanSendEmail(newsletter, emailRecipientFilter
|
|
147
|
+
async checkCanSendEmail(newsletter, emailRecipientFilter) {
|
|
140
148
|
if (!newsletter) {
|
|
141
149
|
throw new errors.EmailError({
|
|
142
150
|
message: tpl(messages.missingNewsletterError),
|
|
@@ -151,59 +159,73 @@ class EmailService {
|
|
|
151
159
|
});
|
|
152
160
|
}
|
|
153
161
|
|
|
154
|
-
const emailCount =
|
|
155
|
-
knownEmailCount === undefined
|
|
156
|
-
? await this.#emailSegmenter.getMembersCount(newsletter, emailRecipientFilter)
|
|
157
|
-
: knownEmailCount;
|
|
162
|
+
const emailCount = await this.#emailSegmenter.getMembersCount(newsletter, emailRecipientFilter);
|
|
158
163
|
await this.checkLimits(emailCount);
|
|
159
164
|
|
|
160
|
-
|
|
165
|
+
const csdEmailCount = this.#domainWarmingService.isEnabled()
|
|
166
|
+
? await this.#domainWarmingService.getWarmupLimit(emailCount)
|
|
167
|
+
: undefined; // Undefined here means domain warming was not used -- distinct from 0
|
|
168
|
+
|
|
169
|
+
return { emailCount, csdEmailCount };
|
|
161
170
|
}
|
|
162
171
|
|
|
163
172
|
/**
|
|
173
|
+
* Creates the pending email for a post being published, from the checks `checkCanSendEmail`
|
|
174
|
+
* ran before the save. Schedule it with `scheduleEmail` once the post and email have committed.
|
|
175
|
+
*
|
|
176
|
+
* It runs no other queries, because the transaction saving the post holds a connection, and
|
|
177
|
+
* on SQLite the only one.
|
|
164
178
|
*
|
|
165
179
|
* @param {Post} post
|
|
166
|
-
* @param {object}
|
|
167
|
-
* @param {EmailPreflight}
|
|
180
|
+
* @param {object} options
|
|
181
|
+
* @param {EmailPreflight|null} options.preflight
|
|
182
|
+
* @param {object} [options.transacting]
|
|
168
183
|
* @returns {Promise<Email>}
|
|
169
184
|
*/
|
|
170
|
-
async createEmail(post, { preflight
|
|
171
|
-
const newsletter =
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const csdEmailCount = this.#domainWarmingService.isEnabled()
|
|
185
|
-
? await this.#domainWarmingService.getWarmupLimit(emailCount)
|
|
186
|
-
: undefined; // Undefined here means domain warming was not used -- distinct from 0
|
|
185
|
+
async createEmail(post, { preflight, transacting }) {
|
|
186
|
+
const { newsletter, emailRecipientFilter, emailCount, csdEmailCount } = preflight ?? {};
|
|
187
|
+
|
|
188
|
+
// Only a concurrent edit can save a newsletter or audience other than the one checked
|
|
189
|
+
if (
|
|
190
|
+
!newsletter ||
|
|
191
|
+
post.get('newsletter_id') !== newsletter.id ||
|
|
192
|
+
post.get('email_recipient_filter') !== emailRecipientFilter
|
|
193
|
+
) {
|
|
194
|
+
throw new errors.UpdateCollisionError({
|
|
195
|
+
message: tpl(messages.postChangedWhilePublishing),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
187
198
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
199
|
+
return this.#models.Email.add(
|
|
200
|
+
{
|
|
201
|
+
post_id: post.id,
|
|
202
|
+
newsletter_id: newsletter.id,
|
|
203
|
+
status: 'pending',
|
|
204
|
+
submitted_at: new Date(),
|
|
205
|
+
track_opens: !!this.#settingsCache.get('email_track_opens'),
|
|
206
|
+
track_clicks: !!this.#settingsCache.get('email_track_clicks'),
|
|
207
|
+
feedback_enabled: !!newsletter.get('feedback_enabled'),
|
|
208
|
+
recipient_filter: emailRecipientFilter,
|
|
209
|
+
subject: this.#emailRenderer.getSubject(post),
|
|
210
|
+
from: this.#emailRenderer.getFromAddress(post, newsletter),
|
|
211
|
+
replyTo: this.#emailRenderer.getReplyToAddress(post, newsletter),
|
|
212
|
+
email_count: emailCount,
|
|
213
|
+
preflight_email_count: emailCount,
|
|
214
|
+
csd_email_count: csdEmailCount,
|
|
215
|
+
source: post.get('lexical') || post.get('mobiledoc'),
|
|
216
|
+
source_type: post.get('lexical') ? 'lexical' : 'mobiledoc',
|
|
217
|
+
},
|
|
218
|
+
{ transacting },
|
|
219
|
+
);
|
|
220
|
+
}
|
|
206
221
|
|
|
222
|
+
/**
|
|
223
|
+
* Starts sending a committed pending email. A scheduling failure is saved on the email.
|
|
224
|
+
*
|
|
225
|
+
* @param {Email} email
|
|
226
|
+
* @returns {Promise<Email>}
|
|
227
|
+
*/
|
|
228
|
+
async scheduleEmail(email) {
|
|
207
229
|
try {
|
|
208
230
|
await this.#batchSendingService.scheduleEmail(email);
|
|
209
231
|
} catch (e) {
|
|
@@ -381,12 +403,7 @@ class EmailService {
|
|
|
381
403
|
});
|
|
382
404
|
}
|
|
383
405
|
|
|
384
|
-
|
|
385
|
-
throw new errors.BadRequestError({
|
|
386
|
-
message: tpl(messages.retryEmailNotFailed),
|
|
387
|
-
code: 'BULK_EMAIL_RETRY_NOT_FAILED',
|
|
388
|
-
});
|
|
389
|
-
}
|
|
406
|
+
await this.checkCanRetryEmail(email.id);
|
|
390
407
|
|
|
391
408
|
await this.checkLimits();
|
|
392
409
|
|
|
@@ -404,7 +421,7 @@ class EmailService {
|
|
|
404
421
|
if (!pendingEmail) {
|
|
405
422
|
throw new errors.BadRequestError({
|
|
406
423
|
message: tpl(messages.retryEmailNotFailed),
|
|
407
|
-
code:
|
|
424
|
+
code: RETRY_NOT_FAILED_CODE,
|
|
408
425
|
});
|
|
409
426
|
}
|
|
410
427
|
|
|
@@ -417,6 +434,28 @@ class EmailService {
|
|
|
417
434
|
return pendingEmail;
|
|
418
435
|
}
|
|
419
436
|
|
|
437
|
+
/**
|
|
438
|
+
* Validates retry eligibility before a retry is queued.
|
|
439
|
+
* @param {string} emailId
|
|
440
|
+
* @returns {Promise<void>}
|
|
441
|
+
*/
|
|
442
|
+
async checkCanRetryEmail(emailId) {
|
|
443
|
+
// Re-read persisted state: the caller's email or eligibility can be stale.
|
|
444
|
+
const eligibility = await this.#sendingStatusService.retryEligibilityFor(emailId);
|
|
445
|
+
if (eligibility === 'not-failed') {
|
|
446
|
+
throw new errors.BadRequestError({
|
|
447
|
+
message: tpl(messages.retryEmailNotFailed),
|
|
448
|
+
code: RETRY_NOT_FAILED_CODE,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
if (eligibility === 'unknown-outcome') {
|
|
452
|
+
throw new errors.BadRequestError({
|
|
453
|
+
message: tpl(messages.retryEmailUnknownOutcome),
|
|
454
|
+
code: RETRY_UNKNOWN_OUTCOME_CODE,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
420
459
|
/**
|
|
421
460
|
* @param {import('./jobs/send-email-job').default} job
|
|
422
461
|
*/
|
|
@@ -587,3 +626,5 @@ class EmailService {
|
|
|
587
626
|
}
|
|
588
627
|
|
|
589
628
|
module.exports = EmailService;
|
|
629
|
+
module.exports.RETRY_UNKNOWN_OUTCOME_CODE = RETRY_UNKNOWN_OUTCOME_CODE;
|
|
630
|
+
module.exports.RETRY_NOT_FAILED_CODE = RETRY_NOT_FAILED_CODE;
|
|
@@ -13,6 +13,7 @@ const SendingResource = zod_1.z.object({
|
|
|
13
13
|
estimated_seconds_remaining: zod_1.z.number().nullable(),
|
|
14
14
|
}),
|
|
15
15
|
failed_during: SendingPhaseResource.optional(),
|
|
16
|
+
retryable: zod_1.z.boolean().optional(),
|
|
16
17
|
});
|
|
17
18
|
const EmailStatusResource = zod_1.z.object({
|
|
18
19
|
id: zod_1.z.string(),
|
|
@@ -26,7 +27,9 @@ exports.toEmailStatusesResponse = sending_status_1.EmailSendingStatus.transform(
|
|
|
26
27
|
sending: {
|
|
27
28
|
status: sending.status,
|
|
28
29
|
progress: (0, case_keys_1.snakeKeys)(sending.progress),
|
|
29
|
-
...(sending.status === 'failed'
|
|
30
|
+
...(sending.status === 'failed'
|
|
31
|
+
? { failed_during: sending.failedDuring, retryable: sending.retryable }
|
|
32
|
+
: {}),
|
|
30
33
|
},
|
|
31
34
|
},
|
|
32
35
|
],
|