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.
Files changed (300) hide show
  1. package/components/ghost-storage-base-3.0.1.tgz +0 -0
  2. package/components/tryghost-adapter-base-cache-0.2.1.tgz +0 -0
  3. package/components/tryghost-adapter-base-scheduling-0.2.5.tgz +0 -0
  4. package/components/tryghost-adapter-base-sso-0.1.4.tgz +0 -0
  5. package/components/tryghost-app-contracts-0.0.0.tgz +0 -0
  6. package/components/tryghost-checkout-0.0.0.tgz +0 -0
  7. package/components/tryghost-i18n-0.0.0.tgz +0 -0
  8. package/components/tryghost-kg-card-factory-5.2.6.tgz +0 -0
  9. package/components/tryghost-kg-clean-basic-html-4.3.6.tgz +0 -0
  10. package/components/tryghost-kg-converters-1.2.6.tgz +0 -0
  11. package/components/tryghost-kg-default-cards-10.3.7.tgz +0 -0
  12. package/components/tryghost-kg-default-nodes-2.2.3.tgz +0 -0
  13. package/components/tryghost-kg-default-transforms-1.3.6.tgz +0 -0
  14. package/components/tryghost-kg-html-to-lexical-1.4.1.tgz +0 -0
  15. package/components/tryghost-kg-lexical-html-renderer-1.5.1.tgz +0 -0
  16. package/components/tryghost-kg-markdown-html-renderer-7.2.7.tgz +0 -0
  17. package/components/tryghost-kg-utils-1.1.6.tgz +0 -0
  18. package/components/tryghost-metafield-csv-0.0.0.tgz +0 -0
  19. package/components/tryghost-metafield-types-0.1.1.tgz +0 -0
  20. package/core/boot.js +6 -0
  21. package/core/built/admin/assets/{about-D_nwtf7X.js → about-Dxp30cNg.js} +1 -1
  22. package/core/built/admin/assets/{account-migration-BaecWw1T.js → account-migration-D05akW4q.js} +1 -1
  23. package/core/built/admin/assets/activitypub/{account-migration-4saTEZ6p.js → account-migration-BmiwuzXI.js} +6 -6
  24. package/core/built/admin/assets/activitypub/activitypub.js +2 -2
  25. package/core/built/admin/assets/activitypub/{bluesky-sharing-BHBNuZQH.js → bluesky-sharing-B1fzMdFb.js} +6 -6
  26. package/core/built/admin/assets/activitypub/{content-formatters-DVx9YktY.js → content-formatters-G0qBT5QE.js} +3 -3
  27. package/core/built/admin/assets/activitypub/{domain-rAmlMXBC.js → domain-B3UtdA1m.js} +7 -7
  28. package/core/built/admin/assets/activitypub/{edit-profile-CXYL4ZrS.js → edit-profile-9EodzviR.js} +4 -4
  29. package/core/built/admin/assets/activitypub/{explore-dqpEx3GA.js → explore-DmwPdxHI.js} +8 -8
  30. package/core/built/admin/assets/activitypub/{feed-Cb-wYNw9.js → feed-yauqnu7A.js} +5 -5
  31. package/core/built/admin/assets/activitypub/{field-JxegcPt6.js → field-CsrMjbg2.js} +2 -2
  32. package/core/built/admin/assets/activitypub/{inbox-2hXDJBn5.js → inbox-DV3RrpFH.js} +7 -7
  33. package/core/built/admin/assets/activitypub/{label-DRQzZ7lb.js → label-DPs40NLn.js} +2 -2
  34. package/core/built/admin/assets/activitypub/layout-CvOhndCA.js +1 -0
  35. package/core/built/admin/assets/activitypub/{moderation-BnYKcTXY.js → moderation-DGa6hVNH.js} +7 -7
  36. package/core/built/admin/assets/activitypub/{note-C6ucS3Wu.js → note-BVL6LVHF.js} +5 -5
  37. package/core/built/admin/assets/activitypub/{notifications-DgBW9wEC.js → notifications-CRUu_uMC.js} +5 -5
  38. package/core/built/admin/assets/activitypub/{onboarding-DGu6yG4L.js → onboarding-Dpm-nLkQ.js} +2 -2
  39. package/core/built/admin/assets/activitypub/{onboarding-Bq3fNf_a.js → onboarding-KtpJA-xb.js} +2 -2
  40. package/core/built/admin/assets/activitypub/{preferences-C3jI1eVN.js → preferences-CdFuVqIt.js} +7 -7
  41. package/core/built/admin/assets/activitypub/{profile-Cn3vRb5Z.js → profile-KnNAoHYu.js} +8 -8
  42. package/core/built/admin/assets/activitypub/{routes-DbrwlS9h.js → routes-BRndai50.js} +26 -26
  43. package/core/built/admin/assets/activitypub/{settings-D0BALDQu.js → settings-DU7ftJ8w.js} +4 -4
  44. package/core/built/admin/assets/activitypub/{site-BFGj8HuQ.js → site-D1ZYV3AT.js} +2 -2
  45. package/core/built/admin/assets/activitypub/{step-1-CaoPg0a5.js → step-1-9IHheE78.js} +4 -4
  46. package/core/built/admin/assets/activitypub/{step-3-5k0TkyC9.js → step-3-DBERnfa-.js} +3 -3
  47. package/core/built/admin/assets/activitypub/{tabs-BV4JU7Vg.js → tabs-CvkwStij.js} +4 -4
  48. package/core/built/admin/assets/activitypub/{topic-filter-71sX5nfN.js → topic-filter-CS0Jsf_4.js} +2 -2
  49. package/core/built/admin/assets/activitypub/{use-activity-pub-queries-B3srUuKt.js → use-activity-pub-queries-CGgtQrxL.js} +2 -2
  50. package/core/built/admin/assets/activitypub/{use-reply-chain-data-Cl6bGNy7.js → use-reply-chain-data-CLdUpx1t.js} +4 -4
  51. package/core/built/admin/assets/activitypub/{users-EcSPpds5.js → users-CyoMXlwX.js} +11 -3
  52. package/core/built/admin/assets/{add-integration-modal-DBFzeAUT.js → add-integration-modal-BVg-ec8i.js} +1 -1
  53. package/core/built/admin/assets/{add-newsletter-modal-CYJe-dTC.js → add-newsletter-modal-Du6th521.js} +1 -1
  54. package/core/built/admin/assets/{add-recommendation-modal-CgwGKH-_.js → add-recommendation-modal-BiCJ0-q5.js} +1 -1
  55. package/core/built/admin/assets/{analytics-CcXT_WqF.js → analytics-CQJzDDdJ.js} +1 -1
  56. package/core/built/admin/assets/{announcement-bar-modal-Ldcg-PB-.js → announcement-bar-modal-CskyZLzT.js} +1 -1
  57. package/core/built/admin/assets/{api-BLteQZ7W.js → api-BFocMbDg.js} +2 -2
  58. package/core/built/admin/assets/{api-keys-CoeerpwI.js → api-keys-CqKJHuAV.js} +1 -1
  59. package/core/built/admin/assets/{audience-Deg9lzfk.js → audience-Dn7LuH-r.js} +1 -1
  60. package/core/built/admin/assets/{auth-layout-CRaMX7g6.js → auth-layout-VLhXgXn5.js} +1 -1
  61. package/core/built/admin/assets/{automated-emails-WcjjRkRV.js → automated-emails-D5cFl1wr.js} +1 -1
  62. package/core/built/admin/assets/{automations-CbIdX0rd.js → automations-BpweKEcq.js} +1 -1
  63. package/core/built/admin/assets/automations-DBTYwNTJ.js +1 -0
  64. package/core/built/admin/assets/{bluesky-sharing-BJMNRwnU.js → bluesky-sharing-CrfpFHQ9.js} +1 -1
  65. package/core/built/admin/assets/{chart-helpers-D-3N170i.js → chart-helpers-BSuFLd6-.js} +1 -1
  66. package/core/built/admin/assets/{chunk.524.94ab17e1199b4c3f4615.js → chunk.524.e24d68bc6253b274b726.js} +4 -4
  67. package/core/built/admin/assets/{chunk.582.6f09b19a007e2805824e.js → chunk.582.47834285406dfa1abb29.js} +6 -6
  68. package/core/built/admin/assets/{chunk.476.53b5794a67d68111e0bf.js → chunk.837.e1454abc054e1fba1fe5.js} +5 -5
  69. package/core/built/admin/assets/{code-editor-view-DhTwpxxv.js → code-editor-view-BrGe3eS4.js} +1 -1
  70. package/core/built/admin/assets/{code-editor-view-C7wXGiuv.js → code-editor-view-CY6JCXNv.js} +1 -1
  71. package/core/built/admin/assets/{color-picker-field-Cjz2zY11.js → color-picker-field-DGe9HH6W.js} +1 -1
  72. package/core/built/admin/assets/{comments-DY-VLVxh.js → comments-iRbF6XiJ.js} +1 -1
  73. package/core/built/admin/assets/{config-B3HuCo0y.js → config-CRAZ4Civ.js} +1 -1
  74. package/core/built/admin/assets/{content-api-modal-2jt-jWiM.js → content-api-modal-B-XRbzeW.js} +1 -1
  75. package/core/built/admin/assets/{content-formatters-VqvNA7Pv.js → content-formatters-C08JJbUJ.js} +2 -2
  76. package/core/built/admin/assets/{context-menu-BWbk9nd5.js → context-menu-BxGXsgQO.js} +1 -1
  77. package/core/built/admin/assets/{custom-field-type-option-CIatEwIN.js → custom-field-type-option-C8Nt4X6x.js} +1 -1
  78. package/core/built/admin/assets/{custom-integration-modal-Cw0w2Tb3.js → custom-integration-modal-DzI16qjN.js} +1 -1
  79. package/core/built/admin/assets/{date-range-select-O-uA-BRV.js → date-range-select-BZqsYsGx.js} +1 -1
  80. package/core/built/admin/assets/{design-and-theme-modal-BLjNHRYN.js → design-and-theme-modal-1ty_KEkM.js} +2 -2
  81. package/core/built/admin/assets/{dist-DJhCsld6.js → dist-B4jqKAG5.js} +1 -1
  82. package/core/built/admin/assets/{dist-5Ol0DC_t.js → dist-Bmo2j_KH.js} +1 -1
  83. package/core/built/admin/assets/{dist-p52UDdoR.js → dist-C-_aTlvJ.js} +1 -1
  84. package/core/built/admin/assets/{dist-CcN1a5SB.js → dist-CWHIoxYn.js} +1 -1
  85. package/core/built/admin/assets/{dist-BcfsaiCV.js → dist-Cp2M5TzT.js} +1 -1
  86. package/core/built/admin/assets/{dist-kjTYGivb.js → dist-DQAAPbtg.js} +1 -1
  87. package/core/built/admin/assets/{dist-IMek6Acg.js → dist-DyO279y5.js} +1 -1
  88. package/core/built/admin/assets/{domain-DaPAeOFF.js → domain-DiFl_Z1A.js} +1 -1
  89. package/core/built/admin/assets/{edit-profile-BKO5qonH.js → edit-profile-PrLbJV3P.js} +1 -1
  90. package/core/built/admin/assets/editor-BBJDJL8U.js +8 -0
  91. package/core/built/admin/assets/{editor-screen-l4BUPE2o.js → editor-screen-nblrp808.js} +3 -3
  92. package/core/built/admin/assets/{email-previews-B1Ska3CF.js → email-previews-BKJxrW29.js} +1 -1
  93. package/core/built/admin/assets/{email-sending-status-provider-Bjn5KTy2.js → email-sending-status-provider-DB9ylBWk.js} +1 -1
  94. package/core/built/admin/assets/{emails-qxNbhO6X.js → emails-8YiSmC2X.js} +1 -1
  95. package/core/built/admin/assets/{embed-signup-form-modal-DSgds6Js.js → embed-signup-form-modal-CZmbYKfd.js} +1 -1
  96. package/core/built/admin/assets/{error-boundary-CJODV8Rg.js → error-boundary-DrxPFFO-.js} +1 -1
  97. package/core/built/admin/assets/{esm-CNLDtBMc.js → esm-_iCUCvbN.js} +1 -1
  98. package/core/built/admin/assets/{explore-C6V_JpUZ.js → explore-C69LSRVg.js} +1 -1
  99. package/core/built/admin/assets/{feed-B_TyFn9E.js → feed-B_0m7WhZ.js} +1 -1
  100. package/core/built/admin/assets/{feedback-gKOzmiGt.js → feedback-DW1DXdzT.js} +1 -1
  101. package/core/built/admin/assets/{filters-Bf6E1qAP.js → filters-Dr3vMnjZ.js} +1 -1
  102. package/core/built/admin/assets/{filters-DhsiEotO.js → filters-M2sRWPoF.js} +1 -1
  103. package/core/built/admin/assets/{first-promoter-modal-LmTXHr-z.js → first-promoter-modal-D43R2zhI.js} +1 -1
  104. package/core/built/admin/assets/{focus-koenig-editor-on-bottom-click-BAcLNJti.js → focus-koenig-editor-on-bottom-click-BzS8jiJf.js} +1 -1
  105. package/core/built/admin/assets/fullscreen-close-button-CWSp-MsK.js +1 -0
  106. package/core/built/admin/assets/{ghost-63bc7da4336844000c523a5a0819531e.js → ghost-ffde29676517b768b1aef5ec8d492418.js} +93 -96
  107. package/core/built/admin/assets/{gift-link-modal-uZtwRHzl.js → gift-link-modal-CNE7dfPD.js} +1 -1
  108. package/core/built/admin/assets/{gift-links-HVqaXear.js → gift-links-lQ7H_D8Z.js} +1 -1
  109. package/core/built/admin/assets/global-search-modal-DzRM3aON.js +1 -0
  110. package/core/built/admin/assets/{growth-BlPW4DHj.js → growth-CAi_5HIf.js} +1 -1
  111. package/core/built/admin/assets/{growth-CflU3vR0.js → growth-CMHbTWKE.js} +1 -1
  112. package/core/built/admin/assets/history-modal-BIz13vhr.js +1 -0
  113. package/core/built/admin/assets/{hooks-C-BDQ6Hs.js → hooks-0r7qxGEo.js} +2 -2
  114. package/core/built/admin/assets/{html-field-D6G0G1d_.js → html-field-vTe2q1be.js} +1 -1
  115. package/core/built/admin/assets/{iframe-buffering-DW-Axhvc.js → iframe-buffering-cu-yK7NA.js} +1 -1
  116. package/core/built/admin/assets/{images-BSUWxquu.js → images-DTZtaIAr.js} +1 -1
  117. package/core/built/admin/assets/{inbox-BkU3sg13.js → inbox-BHfpmmzn.js} +1 -1
  118. package/core/built/admin/assets/{index-z8-nEVdb.css → index-B02LJqc8.css} +1 -1
  119. package/core/built/admin/assets/{index-CuDhPtkB.js → index-RuRs_qGS.js} +3 -3
  120. package/core/built/admin/assets/{index.es-H6HOI4DG.js → index.es-rdb1skp_.js} +1 -1
  121. package/core/built/admin/assets/{integrations-BRo5DwA5.js → integrations-Deg9-u8X.js} +1 -1
  122. package/core/built/admin/assets/{invite-user-modal-CydBOG99.js → invite-user-modal-C4DeNcoO.js} +1 -1
  123. package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +10483 -10455
  124. package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +131 -131
  125. package/core/built/admin/assets/{koenig-lexical-CG_eb4dw.js → koenig-lexical-BhXu1__-.js} +96 -96
  126. package/core/built/admin/assets/{kpi-helpers-CyRx_yD6.js → kpi-helpers-H-Hl6h7h.js} +1 -1
  127. package/core/built/admin/assets/{kpi-tabs-V1_Nu9sh.js → kpi-tabs-k4A9-imn.js} +1 -1
  128. package/core/built/admin/assets/{label-picker-GTIIlBQ6.js → label-picker-DaivLdMw.js} +1 -1
  129. package/core/built/admin/assets/{labels-BVGLgdAj.js → labels-BCj6D_nl.js} +1 -1
  130. package/core/built/admin/assets/{layout-CG58T6P7.js → layout-_g2xhB0L.js} +1 -1
  131. package/core/built/admin/assets/{lib-Dkic19kH.js → lib-Bre6UXdJ.js} +2 -2
  132. package/core/built/admin/assets/{links-jD8AkkIu.js → links-C8RnEJVJ.js} +1 -1
  133. package/core/built/admin/assets/{member-activity-JggQ0VNQ.js → member-activity-DNJPUW5X.js} +1 -1
  134. package/core/built/admin/assets/{member-activity-feed-BBeCZmLM.js → member-activity-feed-CvKEetHS.js} +1 -1
  135. package/core/built/admin/assets/{member-custom-fields-DPsyP1Hq.js → member-custom-fields-CBdiwPD9.js} +1 -1
  136. package/core/built/admin/assets/{member-detail-D7qugPN7.js → member-detail-BIoyIIsl.js} +1 -1
  137. package/core/built/admin/assets/{members-Cj_BUZUL.js → members-CH6KeXQc.js} +1 -1
  138. package/core/built/admin/assets/{members-alDWPKDd.js → members-Dd9RczFK.js} +1 -1
  139. package/core/built/admin/assets/{migrate-2uKKakld.js → migrate-CVQIWsXO.js} +1 -1
  140. package/core/built/admin/assets/{moderation-pCUKsxbF.js → moderation-OtjZoNOc.js} +1 -1
  141. package/core/built/admin/assets/{navigation-modal-D0FBI5-m.js → navigation-modal-Bl02NjtO.js} +1 -1
  142. package/core/built/admin/assets/{newsletter-4urgCSR_.js → newsletter-BXyT7o69.js} +1 -1
  143. package/core/built/admin/assets/{newsletter-detail-modal-Chh1jvjH.js → newsletter-detail-modal-DxuKNu_1.js} +1 -1
  144. package/core/built/admin/assets/{newsletter-emails-CzVCiC2K.js → newsletter-emails-DicgmnMU.js} +1 -1
  145. package/core/built/admin/assets/{newsletters-eRQTtOBH.js → newsletters-By25FmUs.js} +1 -1
  146. package/core/built/admin/assets/{newsletters-BMEgeL2l.js → newsletters-CACa0VCx.js} +1 -1
  147. package/core/built/admin/assets/{note-dqog4Bua.js → note-CF4LrSmR.js} +1 -1
  148. package/core/built/admin/assets/{notifications-rcEVv-gx.js → notifications-BmHJmqIJ.js} +1 -1
  149. package/core/built/admin/assets/{offers-hV7zBtsI.js → offers-Df9Jzay5.js} +1 -1
  150. package/core/built/admin/assets/{offers-container-modal-DgZgkMsA.js → offers-container-modal-R0HRwugm.js} +1 -1
  151. package/core/built/admin/assets/{onboarding-4TxbZb90.js → onboarding-Bpo1yTzA.js} +1 -1
  152. package/core/built/admin/assets/{onboarding-SGscZLIc.js → onboarding-bMq3R8jB.js} +1 -1
  153. package/core/built/admin/assets/{onboarding-route-B3mN9l7E.js → onboarding-route-flf5YWti.js} +1 -1
  154. package/core/built/admin/assets/{overview-D4EanvDG.js → overview-C9WvmmOC.js} +1 -1
  155. package/core/built/admin/assets/{overview-CCwbHet8.js → overview-CDkHdIJM.js} +1 -1
  156. package/core/built/admin/assets/{page-header-Baymg6VR.js → page-header-CxTb0je5.js} +1 -1
  157. package/core/built/admin/assets/{pagemenu--UL0gW1x.js → pagemenu-DXr_U43J.js} +1 -1
  158. package/core/built/admin/assets/{pages-Cbq1M2kG.js → pages-Bm-zFXyP.js} +1 -1
  159. package/core/built/admin/assets/pages-route-LCw94aIO.js +1 -0
  160. package/core/built/admin/assets/{pintura-modal-D6HGlnJa.js → pintura-modal-B5sIPWcj.js} +1 -1
  161. package/core/built/admin/assets/{portal-modal-Bhqsevn_.js → portal-modal-DjOO3Unv.js} +1 -1
  162. package/core/built/admin/assets/{post-analytics-header-DX9fETdi.js → post-analytics-header-D2anehCj.js} +1 -1
  163. package/core/built/admin/assets/{post-analytics-provider-U7RtOle1.js → post-analytics-provider-BLQvV5C0.js} +1 -1
  164. package/core/built/admin/assets/{post-analytics-BJWDtC3a.js → post-analytics-wdu9gq8u.js} +1 -1
  165. package/core/built/admin/assets/{post-debug-D1b8Amju.js → post-debug-BnABXMfD.js} +1 -1
  166. package/core/built/admin/assets/{posts-Lg-p_ZHg.js → posts-CftunZgV.js} +1 -1
  167. package/core/built/admin/assets/{posts-list-screen-CmsW18e_.js → posts-list-screen-CwXmvY68.js} +2 -2
  168. package/core/built/admin/assets/posts-route-J74fMhCe.js +1 -0
  169. package/core/built/admin/assets/{preferences-CMlDfHfa.js → preferences-DFhf_pJF.js} +1 -1
  170. package/core/built/admin/assets/{preview-modal-D8om_cQv.js → preview-modal-BrZza3R_.js} +1 -1
  171. package/core/built/admin/assets/{profile-yFsIoZom.js → profile-CS4fP60N.js} +1 -1
  172. package/core/built/admin/assets/{radio-group-DwgLjwWB.js → radio-group-Xfp-frJz.js} +1 -1
  173. package/core/built/admin/assets/{referrers-DzP5QoXL.js → referrers-C8LJlrBm.js} +1 -1
  174. package/core/built/admin/assets/{reload-CZ-KP3IQ.js → reload-D0fnKoCm.js} +1 -1
  175. package/core/built/admin/assets/{reset-CCAa8v35.js → reset-CB-ATcLE.js} +1 -1
  176. package/core/built/admin/assets/{restore-screen-Ba0i2Y1K.js → restore-screen-EaZf4V8j.js} +1 -1
  177. package/core/built/admin/assets/{routes-DrnhmKTT.js → routes-DKfpFMfF.js} +2 -2
  178. package/core/built/admin/assets/search-index-sM3OjEUl.js +1 -0
  179. package/core/built/admin/assets/settings-BWzod-5X.js +4 -0
  180. package/core/built/admin/assets/{settings-lqWkfSmT.js → settings-DvgngbFt.js} +1 -1
  181. package/core/built/admin/assets/{setup-BopRPuaM.js → setup-CdHAchG5.js} +1 -1
  182. package/core/built/admin/assets/{signin-e8NgsU9M.js → signin-CUEEme88.js} +1 -1
  183. package/core/built/admin/assets/{signin-verify-CHkFJevb.js → signin-verify-CYRphSrj.js} +1 -1
  184. package/core/built/admin/assets/{signout-DRvQZuWa.js → signout-kLCJau9G.js} +1 -1
  185. package/core/built/admin/assets/{signup-DQHi7jZz.js → signup-J2Vjq7Gt.js} +1 -1
  186. package/core/built/admin/assets/{slack-modal-BuGESzgn.js → slack-modal-DKamr_94.js} +1 -1
  187. package/core/built/admin/assets/source-icon-DqBCPDIK.js +1 -0
  188. package/core/built/admin/assets/{stats-CHOikfoP.js → stats-BL_FJ7Ns.js} +1 -1
  189. package/core/built/admin/assets/{stats-view-DQZzKeEz.js → stats-view-BwbdkdOQ.js} +1 -1
  190. package/core/built/admin/assets/{step-1-C8XwHcKt.js → step-1-CLYhNmG9.js} +1 -1
  191. package/core/built/admin/assets/{step-2-BXaXAdwY.js → step-2-B2ujMlpa.js} +1 -1
  192. package/core/built/admin/assets/{step-3-DytbqpJi.js → step-3-DoaTWqqj.js} +1 -1
  193. package/core/built/admin/assets/{stripe-connect-modal-BhudjSeO.js → stripe-connect-modal-DytxH9DJ.js} +1 -1
  194. package/core/built/admin/assets/{tabs-CDb_TceL.js → tabs-fHs3A8WC.js} +1 -1
  195. package/core/built/admin/assets/{tag-detail-V72Zlro8.js → tag-detail-BcO46Ids.js} +2 -2
  196. package/core/built/admin/assets/{tag-picker-M3fC-yOF.js → tag-picker-Ct6NeqWK.js} +1 -1
  197. package/core/built/admin/assets/{tags-CXqE2A2T.js → tags-C1h3wCpW.js} +1 -1
  198. package/core/built/admin/assets/{tags-ZhqbPPc6.js → tags-OlmRJ6Ps.js} +1 -1
  199. package/core/built/admin/assets/{testimonials-modal-Cn8gvIFA.js → testimonials-modal-DMsQzSCB.js} +1 -1
  200. package/core/built/admin/assets/{themes-pOHHvnkt.js → themes-CPJSMo_W.js} +1 -1
  201. package/core/built/admin/assets/{tier-detail-modal-CeDla1OO.js → tier-detail-modal-DssSmhkF.js} +1 -1
  202. package/core/built/admin/assets/{tiers-D7aGzwZ-.js → tiers-DlvS8D6_.js} +1 -1
  203. package/core/built/admin/assets/{toggle-group-D7ly9OGa.js → toggle-group-BFWCtH3p.js} +1 -1
  204. package/core/built/admin/assets/{topic-filter-DQ3pr6AB.js → topic-filter-DAJ-DFPb.js} +1 -1
  205. package/core/built/admin/assets/{transistor-modal-4_Yx2vRo.js → transistor-modal-CtyFRsD0.js} +1 -1
  206. package/core/built/admin/assets/{unsplash-modal-Dm7R4k4k.js → unsplash-modal-DHnnTPmZ.js} +1 -1
  207. package/core/built/admin/assets/{use-activity-pub-queries-Gy9_locM.js → use-activity-pub-queries-BgR1USeY.js} +1 -1
  208. package/core/built/admin/assets/{use-analytics-data-oJ2o0pSS.js → use-analytics-data-Byf83QE-.js} +1 -1
  209. package/core/built/admin/assets/{use-availability-BqL7Dvn_.js → use-availability-CgZC6L_V.js} +1 -1
  210. package/core/built/admin/assets/{use-check-theme-limit-error-CxC9uKJz.js → use-check-theme-limit-error-DjprzKAN.js} +1 -1
  211. package/core/built/admin/assets/use-definitions-CJuXLSob.js +1 -0
  212. package/core/built/admin/assets/{use-email-sending-status-BFAg5TIb.js → use-email-sending-status-7kopNcza.js} +1 -1
  213. package/core/built/admin/assets/{use-filter-params-BOo9yZ5j.js → use-filter-params-DQ7uX-Zp.js} +1 -1
  214. package/core/built/admin/assets/{use-filterable-api-BnNLaS_r.js → use-filterable-api-CDrrxoGD.js} +1 -1
  215. package/core/built/admin/assets/{use-growth-stats-BS62uyyn.js → use-growth-stats-CCpnBgqS.js} +1 -1
  216. package/core/built/admin/assets/{use-host-limits-CNh4AAEs.js → use-host-limits-BZx7rzRI.js} +1 -1
  217. package/core/built/admin/assets/{use-limiter-DPKxoO1_.js → use-limiter-gF219BEg.js} +1 -1
  218. package/core/built/admin/assets/{use-pintura-config-CyVO62ZV.js → use-pintura-config-D-pmGkn2.js} +1 -1
  219. package/core/built/admin/assets/{use-pintura-editor-B5VBPbiO.js → use-pintura-editor-D_4zhlGE.js} +1 -1
  220. package/core/built/admin/assets/{use-post-referrers-nUPolSk7.js → use-post-referrers-Bc8eq4uy.js} +1 -1
  221. package/core/built/admin/assets/{use-post-top-links-DCH4eO11.js → use-post-top-links-DjltnQuI.js} +1 -1
  222. package/core/built/admin/assets/{use-reply-chain-data-UMzFbDvh.js → use-reply-chain-data-BiV3Cl0B.js} +1 -1
  223. package/core/built/admin/assets/use-sending-eta-gE-2B0Zw.js +1 -0
  224. package/core/built/admin/assets/{use-setting-group-Bf3vY8br.js → use-setting-group-YP84CqrJ.js} +1 -1
  225. package/core/built/admin/assets/{use-staff-users-CcQzTHqH.js → use-staff-users-CnwfdSx1.js} +1 -1
  226. package/core/built/admin/assets/{use-tinybird-query-CPy5ipAP.js → use-tinybird-query-BYbl5gKY.js} +1 -1
  227. package/core/built/admin/assets/{use-tinybird-token-DVNHuhu_.js → use-tinybird-token-Dz37zvb7.js} +1 -1
  228. package/core/built/admin/assets/{use-unsaved-changes-guard-Bvcp_Rny.js → use-unsaved-changes-guard-BkCuSlvU.js} +1 -1
  229. package/core/built/admin/assets/{use-upgrade-route-Bsxw9ZLN.js → use-upgrade-route-C-V-Xfak.js} +1 -1
  230. package/core/built/admin/assets/{use-url-input-DPJ592FB.js → use-url-input-BeYKYMGh.js} +1 -1
  231. package/core/built/admin/assets/{user-detail-modal-6UY6GdNy.js → user-detail-modal-CSoj7vOC.js} +1 -1
  232. package/core/built/admin/assets/{users-CzgQKbfO.js → users-CKJaAxEJ.js} +1 -1
  233. package/core/built/admin/assets/{view-site-DA_-zfGq.js → view-site-SpMx4P7G.js} +1 -1
  234. package/core/built/admin/assets/{web-Cq5YiD2r.js → web-CZf-k4F0.js} +1 -1
  235. package/core/built/admin/assets/{web-DqFIrOxt.js → web-uAqgYPK9.js} +1 -1
  236. package/core/built/admin/assets/{zapier-modal-DgqU-OBB.js → zapier-modal-DENbgTyE.js} +1 -1
  237. package/core/built/admin/index.html +24 -24
  238. package/core/frontend/helpers/color_to_rgba.js +15 -13
  239. package/core/frontend/helpers/facebook_url.js +27 -15
  240. package/core/frontend/services/assets-minification/card-assets.js +1 -1
  241. package/core/frontend/services/helpers/register-ghost-helpers.js +2 -2
  242. package/core/server/api/endpoints/app-installations.js +36 -0
  243. package/core/server/api/endpoints/automations.js +1 -1
  244. package/core/server/api/endpoints/index.js +4 -0
  245. package/core/server/data/exporter/table-lists.js +2 -0
  246. package/core/server/data/migrations/versions/6.69/2026-10-07-06-20-43-add-app-installation-permissions.js +40 -0
  247. package/core/server/data/schema/fixtures/fixtures.json +21 -0
  248. package/core/server/data/schema/in-development.js +1 -1
  249. package/core/server/data/schema/schema.js +53 -0
  250. package/core/server/lib/after-commit.js +20 -0
  251. package/core/server/models/action.js +6 -0
  252. package/core/server/services/app-installations/actions.js +44 -0
  253. package/core/server/services/app-installations/codec.js +20 -0
  254. package/core/server/services/app-installations/index.js +42 -0
  255. package/core/server/services/app-installations/schema.js +52 -0
  256. package/core/server/services/app-installations/service.js +160 -0
  257. package/core/server/services/automations/README.md +3 -3
  258. package/core/server/services/automations/automations-api.js +1 -1
  259. package/core/server/services/comments/comments-service-emails.js +8 -6
  260. package/core/server/services/email-service/README.md +16 -0
  261. package/core/server/services/email-service/email-service-wrapper.js +1 -0
  262. package/core/server/services/email-service/email-service.js +94 -53
  263. package/core/server/services/email-service/sending-status-serializers.js +4 -1
  264. package/core/server/services/email-service/sending-status-service.js +13 -0
  265. package/core/server/services/email-service/sending-status.js +12 -1
  266. package/core/server/services/members/members-api/repositories/member-repository.js +5 -23
  267. package/core/server/services/posts/post-email-handler.js +61 -28
  268. package/core/server/services/posts/posts-service.js +27 -2
  269. package/core/server/services/service-inventory.yaml +93 -7
  270. package/core/server/services/stats/stats-service.js +1 -5
  271. package/core/server/services/tinybird-sync/index.js +0 -3
  272. package/core/server/services/tinybird-sync/tinybird-sync-service.js +2 -6
  273. package/core/server/web/api/endpoints/admin/routes.js +25 -12
  274. package/core/shared/labs.js +4 -6
  275. package/package.json +11 -10
  276. package/pnpm-lock.yaml +112 -100
  277. package/pnpm-workspace.yaml +14 -12
  278. package/components/ghost-storage-base-3.0.0.tgz +0 -0
  279. package/components/tryghost-adapter-base-cache-0.2.0.tgz +0 -0
  280. package/components/tryghost-checkout-0.0.1.tgz +0 -0
  281. package/components/tryghost-kg-card-factory-5.2.4.tgz +0 -0
  282. package/components/tryghost-kg-clean-basic-html-4.3.4.tgz +0 -0
  283. package/components/tryghost-kg-converters-1.2.5.tgz +0 -0
  284. package/components/tryghost-kg-default-transforms-1.3.4.tgz +0 -0
  285. package/components/tryghost-kg-html-to-lexical-1.4.0.tgz +0 -0
  286. package/components/tryghost-kg-markdown-html-renderer-7.2.6.tgz +0 -0
  287. package/components/tryghost-kg-utils-1.1.4.tgz +0 -0
  288. package/components/tryghost-metafield-csv-0.0.1.tgz +0 -0
  289. package/core/built/admin/assets/activitypub/layout-QTbY0t8e.js +0 -1
  290. package/core/built/admin/assets/automations-DVToctfJ.js +0 -1
  291. package/core/built/admin/assets/editor-CMXR0YvR.js +0 -8
  292. package/core/built/admin/assets/fullscreen-close-button-DXssPeDB.js +0 -1
  293. package/core/built/admin/assets/global-search-modal-C8LVJmiz.js +0 -1
  294. package/core/built/admin/assets/history-modal-parBtjpz.js +0 -1
  295. package/core/built/admin/assets/pages-route-CDk3V9rq.js +0 -1
  296. package/core/built/admin/assets/posts-route-CexHhklY.js +0 -1
  297. package/core/built/admin/assets/settings-xqakrAWa.js +0 -4
  298. package/core/built/admin/assets/source-icon-BjVangBd.js +0 -1
  299. package/core/built/admin/assets/use-definitions-DMVU3mTG.js +0 -1
  300. package/core/built/admin/assets/use-sending-eta-BsnKdQhv.js +0 -1
@@ -35,6 +35,19 @@ class SendingStatusService {
35
35
  }, batches),
36
36
  };
37
37
  }
38
+ /** The retry decision statusFor reports, without the recipient counts its progress needs. */
39
+ async retryEligibilityFor(emailId) {
40
+ const email = await this.#knex('emails').select('status').where('id', emailId).first();
41
+ if (!email) {
42
+ return 'not-failed';
43
+ }
44
+ if (sending_status_schema_1.StoredSendingStatus.parse(email.status) !== 'failed') {
45
+ return 'not-failed';
46
+ }
47
+ const batches = await this.#knex('email_batches').distinct('status').where('email_id', emailId);
48
+ const statuses = batches.map((batch) => sending_status_schema_1.StoredSendingStatus.parse(batch.status));
49
+ return (0, sending_status_1.isRetryable)(statuses) ? 'retryable' : 'unknown-outcome';
50
+ }
38
51
  async #batchesFor(emailId, recipientAccounting) {
39
52
  if (recipientAccounting) {
40
53
  const rows = await this.#knex('email_batches')
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.EmailSendingStatus = exports.SendingStatus = exports.SendingProgress = exports.SendingPhase = void 0;
4
4
  exports.buildSendingStatus = buildSendingStatus;
5
+ exports.isRetryable = isRetryable;
5
6
  const zod_1 = require("zod");
6
7
  const ETA_WINDOW_MS = 60_000;
7
8
  const ETA_MIN_INTERVALS = 2;
@@ -14,7 +15,12 @@ exports.SendingProgress = zod_1.z.object({
14
15
  exports.SendingStatus = zod_1.z.discriminatedUnion('status', [
15
16
  zod_1.z.object({ status: exports.SendingPhase, progress: exports.SendingProgress }),
16
17
  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
+ zod_1.z.object({
19
+ status: zod_1.z.literal('failed'),
20
+ progress: exports.SendingProgress,
21
+ failedDuring: exports.SendingPhase,
22
+ retryable: zod_1.z.boolean(),
23
+ }),
18
24
  ]);
19
25
  exports.EmailSendingStatus = zod_1.z.object({
20
26
  id: zod_1.z.string(),
@@ -49,6 +55,7 @@ function buildSendingStatus(email, batches) {
49
55
  status: 'failed',
50
56
  progress: { completed, total, estimatedSecondsRemaining: null },
51
57
  failedDuring: phase,
58
+ retryable: isRetryable(batches.map((batch) => batch.status)),
52
59
  };
53
60
  }
54
61
  const failedThisAttempt = batches.filter((batch) => failedDuringAttempt(batch, attemptStartedAt));
@@ -70,6 +77,10 @@ function buildSendingStatus(email, batches) {
70
77
  },
71
78
  };
72
79
  }
80
+ // A submitting batch may already have been accepted by the provider.
81
+ function isRetryable(batchStatuses) {
82
+ return !batchStatuses.includes('submitting');
83
+ }
73
84
  function sumRecipients(batches) {
74
85
  return batches.reduce((sum, batch) => sum + (batch.recipientCount ?? 0), 0);
75
86
  }
@@ -17,6 +17,7 @@ const { NotFoundError } = require('@tryghost/errors');
17
17
  const validator = require('@tryghost/validator');
18
18
  const crypto = require('crypto');
19
19
  const { hasActiveOffer } = require('../utils/has-active-offer');
20
+ const { afterCommit } = require('../../../../lib/after-commit');
20
21
  const {
21
22
  StartAutomationsPollEvent,
22
23
  } = require('../../../automations/events/start-automations-poll-event');
@@ -239,25 +240,6 @@ module.exports = class MemberRepository {
239
240
  );
240
241
  }
241
242
 
242
- /**
243
- * Runs `fn` once the work it follows has committed.
244
- *
245
- * Without a transaction each query commits as it runs, so `fn` runs now, and whatever it
246
- * throws or returns is the caller's. Inside `options.transacting` it runs when that
247
- * transaction commits, after the caller has moved on, and not at all if it rolls back;
248
- * a rollback, or a failure in `fn`, goes to `onFailure`.
249
- *
250
- * @param {{transacting?: {executionPromise: Promise<unknown>}}} options
251
- * @param {() => unknown} fn
252
- * @param {(err: unknown) => void} onFailure
253
- */
254
- afterCommit(options, fn, onFailure) {
255
- if (!options?.transacting) {
256
- return fn();
257
- }
258
- options.transacting.executionPromise.then(() => fn()).catch(onFailure);
259
- }
260
-
261
243
  /**
262
244
  * @param {Parameters<typeof DomainEvents.dispatch>[0]} event
263
245
  * @param {object} options
@@ -266,8 +248,8 @@ module.exports = class MemberRepository {
266
248
  * @returns {void}
267
249
  */
268
250
  dispatchEvent(event, options) {
269
- this.afterCommit(
270
- options,
251
+ afterCommit(
252
+ options?.transacting,
271
253
  () => DomainEvents.dispatch(event),
272
254
  (err) => {
273
255
  let memberMessageFragment = '';
@@ -1104,8 +1086,8 @@ module.exports = class MemberRepository {
1104
1086
 
1105
1087
  if (this._stripeAPIService.configured && member._changed.email) {
1106
1088
  // Stripe can't be rolled back, so it only hears about an address Ghost has saved.
1107
- await this.afterCommit(
1108
- options,
1089
+ await afterCommit(
1090
+ options?.transacting,
1109
1091
  () => this.updateStripeCustomerEmails(member),
1110
1092
  (err) => {
1111
1093
  logging.error(
@@ -1,5 +1,10 @@
1
1
  const { BadRequestError } = require('@tryghost/errors');
2
+ const logging = require('@tryghost/logging');
2
3
  const tpl = require('@tryghost/tpl');
4
+ const {
5
+ RETRY_UNKNOWN_OUTCOME_CODE,
6
+ RETRY_NOT_FAILED_CODE,
7
+ } = require('../email-service/email-service');
3
8
 
4
9
  const messages = {
5
10
  invalidEmailSegment: "The email segment parameter doesn't contain a valid filter",
@@ -22,7 +27,7 @@ class PostEmailHandler {
22
27
  * Validates email can be sent before saving the post (if an email will be sent)
23
28
  *
24
29
  * @param {import('@tryghost/api-framework').Frame} frame
25
- * @returns {Promise<{newsletter: Object, emailRecipientFilter: string, emailCount: number}|null>}
30
+ * @returns {Promise<import('../email-service/email-service').EmailPreflight|null>}
26
31
  */
27
32
  async validateBeforeSave(frame) {
28
33
  const newStatus = frame.data.posts[0].status;
@@ -33,30 +38,36 @@ class PostEmailHandler {
33
38
 
34
39
  const existingPost = await this.models.Post.findOne(
35
40
  { id: frame.options.id, status: 'all' },
36
- { columns: ['id', 'status', 'newsletter_id', 'email_recipient_filter'] },
41
+ {
42
+ columns: ['id', 'status', 'newsletter_id', 'email_recipient_filter'],
43
+ transacting: frame.options.transacting,
44
+ },
37
45
  );
38
46
  const previousStatus = existingPost?.get('status');
47
+ const existingNewsletterId = existingPost?.get('newsletter_id');
39
48
 
40
- const hasNewsletter = frame.options.newsletter || existingPost?.get('newsletter_id');
49
+ const hasNewsletter = frame.options.newsletter || existingNewsletterId;
41
50
  const sendingEmail = hasNewsletter && this.shouldSendEmail(newStatus, previousStatus);
42
51
 
43
52
  if (!sendingEmail) {
44
53
  return null;
45
54
  }
46
55
 
47
- const emailRecipientFilter =
48
- frame.options.email_segment || existingPost?.get('email_recipient_filter') || 'all';
56
+ // A post keeps the newsletter and audience it already has, as the Post model does on save
57
+ const emailRecipientFilter = existingNewsletterId
58
+ ? existingPost.get('email_recipient_filter')
59
+ : frame.options.email_segment || 'all';
49
60
 
50
61
  await this.validateEmailRecipientFilter(emailRecipientFilter);
51
62
 
52
63
  const newsletter = await this.getNewsletter(frame, existingPost);
53
64
 
54
- const { emailCount } = await this.emailService.checkCanSendEmail(
65
+ const { emailCount, csdEmailCount } = await this.emailService.checkCanSendEmail(
55
66
  newsletter,
56
67
  emailRecipientFilter,
57
68
  );
58
69
 
59
- return { newsletter, emailRecipientFilter, emailCount };
70
+ return { newsletter, emailRecipientFilter, emailCount, csdEmailCount };
60
71
  }
61
72
 
62
73
  /**
@@ -81,36 +92,42 @@ class PostEmailHandler {
81
92
  }
82
93
 
83
94
  /**
84
- * Retrieves the newsletter for the post
95
+ * Retrieves the newsletter the post will be saved with: the one it has, otherwise the one requested
85
96
  *
86
97
  * @param {import('@tryghost/api-framework').Frame} frame
87
98
  * @param {Object|null} existingPost
88
99
  * @returns {Promise<Object|null>}
89
100
  */
90
101
  async getNewsletter(frame, existingPost) {
91
- if (frame.options.newsletter) {
92
- return this.models.Newsletter.findOne({ slug: frame.options.newsletter });
102
+ if (frame.options.newsletter && !existingPost?.get('newsletter_id')) {
103
+ return this.models.Newsletter.findOne(
104
+ { slug: frame.options.newsletter },
105
+ { transacting: frame.options.transacting },
106
+ );
93
107
  }
94
108
 
95
109
  if (existingPost?.get('newsletter_id')) {
96
- return this.models.Newsletter.findOne({ id: existingPost.get('newsletter_id') });
110
+ return this.models.Newsletter.findOne(
111
+ { id: existingPost.get('newsletter_id') },
112
+ { transacting: frame.options.transacting },
113
+ );
97
114
  }
98
115
 
99
116
  return null;
100
117
  }
101
118
 
102
119
  /**
103
- * Handles creating or retrying the newsletter email after post is saved
120
+ * Creates a new newsletter email inside the transaction that publishes the post, so the post
121
+ * is only published if its email is created. Sending, or retrying a failed email, waits for
122
+ * the commit.
104
123
  *
105
- * @param {Object} model - The post model
124
+ * @param {Object} model - The saved post model
106
125
  * @param {Object} [options]
107
- * @param {Object} [options.preflight] - Validation result from validateBeforeSave, passed through to createEmail
108
- * @param {Object} [options.preflight.newsletter]
109
- * @param {string} [options.preflight.emailRecipientFilter]
110
- * @param {number} [options.preflight.emailCount]
111
- * @returns {Promise<void>}
126
+ * @param {import('../email-service/email-service').EmailPreflight|null} [options.preflight] - Result of validateBeforeSave, passed through to createEmail
127
+ * @param {object} [options.transacting]
128
+ * @returns {Promise<(() => Promise<void>)|undefined>} Sends the email; call it once the transaction has committed
112
129
  */
113
- async createOrRetryEmail(model, { preflight } = {}) {
130
+ async createOrRetryEmail(model, { preflight, transacting } = {}) {
114
131
  if (!model.get('newsletter_id')) {
115
132
  return;
116
133
  }
@@ -123,25 +140,41 @@ class PostEmailHandler {
123
140
  }
124
141
 
125
142
  const postEmail = model.relations.email;
126
- let email;
127
143
 
128
144
  if (!postEmail) {
129
- email = await this.emailService.createEmail(model, { preflight });
130
- } else if (postEmail.get('status') === 'failed') {
131
- email = await this.#retryEmail(postEmail);
132
- }
133
-
134
- if (email) {
145
+ const email = await this.emailService.createEmail(model, { preflight, transacting });
135
146
  model.relations.email = email;
136
147
  model.set('email', email);
148
+ return async () => {
149
+ await this.emailService.scheduleEmail(email);
150
+ };
151
+ }
152
+
153
+ if (postEmail.get('status') === 'failed') {
154
+ // Retrying reads the post outside the transaction, so it must be published first.
155
+ return async () => {
156
+ const email = await this.#retryEmail(postEmail, model.id);
157
+ if (email) {
158
+ model.relations.email = email;
159
+ model.set('email', email);
160
+ }
161
+ };
137
162
  }
138
163
  }
139
164
 
140
- async #retryEmail(email) {
165
+ async #retryEmail(email, postId) {
141
166
  try {
142
167
  return await this.emailService.retryEmail(email);
143
168
  } catch (err) {
144
- if (err.code !== 'BULK_EMAIL_RETRY_NOT_FAILED') {
169
+ if (err.code === RETRY_UNKNOWN_OUTCOME_CODE) {
170
+ // The post is already saved. An unknown delivery outcome only withholds
171
+ // the resend, leaving the email failed.
172
+ logging.warn(
173
+ `Post ${postId} was saved without retrying email ${email.id}: delivery outcome is unknown`,
174
+ );
175
+ return;
176
+ }
177
+ if (err.code !== RETRY_NOT_FAILED_CODE) {
145
178
  throw err;
146
179
  }
147
180
  // Publishing is already committed. A concurrent retry that claimed the
@@ -6,6 +6,8 @@ const ObjectId = require('bson-objectid').default;
6
6
  const pick = require('lodash/pick');
7
7
  const DomainEvents = require('@tryghost/domain-events');
8
8
  const PostEmailHandler = require('./post-email-handler');
9
+ const { afterCommit } = require('../../lib/after-commit');
10
+ const logging = require('@tryghost/logging');
9
11
  const {
10
12
  validateAdminApiBulkFilterTransformer,
11
13
  } = require('../../api/endpoints/utils/api-filter-utils');
@@ -75,9 +77,32 @@ class PostsService {
75
77
  async editPost(frame, options) {
76
78
  const preflight = await this.postEmailHandler.validateBeforeSave(frame);
77
79
 
78
- const model = await this.models.Post.edit(frame.data.posts[0], frame.options);
80
+ const save = async (transacting) => {
81
+ const model = await this.models.Post.edit(frame.data.posts[0], {
82
+ ...frame.options,
83
+ transacting,
84
+ });
85
+ const sendEmail = await this.postEmailHandler.createOrRetryEmail(model, {
86
+ preflight,
87
+ transacting,
88
+ });
89
+ return { model, sendEmail };
90
+ };
91
+
92
+ const { model, sendEmail } = frame.options.transacting
93
+ ? await save(frame.options.transacting)
94
+ : await this.models.Post.transaction(save);
79
95
 
80
- await this.postEmailHandler.createOrRetryEmail(model, { preflight });
96
+ if (sendEmail && frame.options.transacting) {
97
+ // The caller commits later. Rolling back leaves nothing to send.
98
+ afterCommit(
99
+ frame.options.transacting,
100
+ () => sendEmail().catch((err) => logging.error(err)),
101
+ () => {},
102
+ );
103
+ } else {
104
+ await sendEmail?.();
105
+ }
81
106
 
82
107
  const dto = model.toJSON(frame.options);
83
108
 
@@ -154,6 +154,50 @@ services:
154
154
  - >-
155
155
  Add same-instance settings-change and root export characterization before moving
156
156
  construction to boot.
157
+ app-installations:
158
+ kind: directory
159
+ disposition: retain
160
+ status: auditing
161
+ entryPoints:
162
+ - app-installations/index.ts
163
+ notes: >-
164
+ Composition-only service over a table that is still in development; preserve the
165
+ availability check that keeps it dormant wherever the table does not exist.
166
+ contracts:
167
+ exports:
168
+ summary: >-
169
+ Named exports are init, the mutable service binding, isAvailable and
170
+ actingContext. The service binding is undefined before init. The Express guard
171
+ that asks isAvailable lives with the Admin API routes in server/web.
172
+ sources:
173
+ - core/server/services/app-installations/index.ts
174
+ tests:
175
+ - test/e2e-api/admin/app-installations.test.ts
176
+ construction:
177
+ summary: >-
178
+ Boot calls synchronous init after database setup. It constructs one
179
+ AppInstallationsService from knex, the Action model and a manifest-rules getter;
180
+ repeated init returns once the binding exists.
181
+ sources:
182
+ - core/server/services/app-installations/index.ts
183
+ - core/boot.js
184
+ tests: []
185
+ configuration:
186
+ summary: >-
187
+ The site and Admin URLs and the development flag are read on every install, not
188
+ captured at construction. Availability is read per request from config.
189
+ sources:
190
+ - core/server/services/app-installations/index.ts
191
+ - core/server/data/schema/in-development.ts
192
+ tests:
193
+ - test/e2e-api/admin/app-installations.test.ts
194
+ unverified:
195
+ - consumers
196
+ - startup
197
+ - lifetime
198
+ - resources
199
+ - failures
200
+ blockers: []
157
201
  audience-feedback:
158
202
  kind: directory
159
203
  disposition: retain
@@ -754,9 +798,26 @@ services:
754
798
  - core/server/services/email-service/email-service-wrapper.js
755
799
  - core/boot.js
756
800
  tests: []
801
+ startup:
802
+ summary: >-
803
+ Boot initializes the wrapper, registers its send-email handler and starts jobs
804
+ before readiness. Outside test environments, interrupted-send recovery runs after
805
+ the app is mounted and readiness is signalled. Recovery is awaited within the
806
+ background sequence, but pending recovery does not delay the boot return or HTTP
807
+ availability; later background initialization continues after it resolves.
808
+ Unit orchestration tests cover initialization order, the test-environment gate
809
+ and sequencing after recovery. Handler-registration tests cover send-email
810
+ routing and its queue; real-boot HTTP tests cover jobs readiness and pending recovery.
811
+ sources:
812
+ - core/boot.js
813
+ - core/server/services/email-service/email-service-wrapper.js
814
+ tests:
815
+ - test/unit/boot-email-analytics.test.ts
816
+ - test/unit/server/services/jobs-service/register-job-handlers.test.ts
817
+ - test/e2e-server/boot-readiness.isolated.test.ts
818
+ - test/e2e-server/boot-background.isolated.test.ts
757
819
  unverified:
758
820
  - consumers
759
- - startup
760
821
  - lifetime
761
822
  - configuration
762
823
  - resources
@@ -768,6 +829,10 @@ services:
768
829
  - >-
769
830
  Audit locale-listener disposal and retained batch-sender state across restart, plus
770
831
  the final status write after tracked batch work.
832
+ - >-
833
+ The boot test controls interrupted-send recovery and subsequent external effects.
834
+ It does not characterize recovery work, background failure handling, recurring
835
+ scheduling or background completion during shutdown.
771
836
  email-suppression-list:
772
837
  kind: directory
773
838
  disposition: retain
@@ -2777,17 +2842,31 @@ services:
2777
2842
  - core/server/services/route-settings/dynamic-routing-service.js
2778
2843
  - core/boot.js
2779
2844
  tests: []
2845
+ startup:
2846
+ summary: >-
2847
+ Boot configures routing in backend-only and frontend-only modes. The isolated HTTP
2848
+ tests cover configured post/tag URLs without a frontend, routes upload refreshing
2849
+ URLs through the same Content API consumer, and frontend rendering with the
2850
+ configured canonical URL. These exercise FileStore and normal boot wiring.
2851
+ Unit orchestration tests separately assert that initDynamicRouting waits for
2852
+ route-settings start and propagates its original error in both frontend modes.
2853
+ sources:
2854
+ - core/boot.js
2855
+ - core/server/services/route-settings/dynamic-routing-service.js
2856
+ tests:
2857
+ - test/unit/boot-dynamic-routing.test.ts
2858
+ - test/e2e-server/boot-backend-only.isolated.test.ts
2859
+ - test/e2e-server/boot-frontend-only.isolated.test.ts
2780
2860
  unverified:
2781
2861
  - consumers
2782
- - startup
2783
2862
  - lifetime
2784
2863
  - configuration
2785
2864
  - resources
2786
2865
  - failures
2787
2866
  blockers:
2788
2867
  - >-
2789
- Characterize repeated configuration/start and upload reload with actual boot and
2790
- adapter ownership; parser helpers are independently consumed before service startup.
2868
+ Characterize repeated configuration/start, reload failure paths and adapter ownership;
2869
+ parser helpers are independently consumed before service startup.
2791
2870
  settings:
2792
2871
  kind: directory
2793
2872
  disposition: retain
@@ -3595,12 +3674,19 @@ services:
3595
3674
  tests: []
3596
3675
  startup:
3597
3676
  summary: >-
3598
- initDynamicRouting gives the shared URL service to RouterManager even without a
3599
- frontend, then awaits route-settings start.
3677
+ Unit orchestration tests assert that initDynamicRouting gives the shared URL
3678
+ service to RouterManager without a frontend, passes the same router and URL
3679
+ instances to route-settings start, and waits for it in both frontend modes.
3680
+ They also assert propagation of the original startup error. The HTTP tests cover
3681
+ configured post/tag URLs in backend-only mode, URLs after upload through the same
3682
+ Content API consumer, and canonical URLs rendered by the frontend.
3600
3683
  sources:
3601
3684
  - core/boot.js
3602
3685
  - core/server/services/route-settings/dynamic-routing-service.js
3603
- tests: []
3686
+ tests:
3687
+ - test/unit/boot-dynamic-routing.test.ts
3688
+ - test/e2e-server/boot-backend-only.isolated.test.ts
3689
+ - test/e2e-server/boot-frontend-only.isolated.test.ts
3604
3690
  unverified:
3605
3691
  - consumers
3606
3692
  - lifetime
@@ -246,14 +246,10 @@ class StatsService {
246
246
  const request = deps.request || require('../../lib/request-external');
247
247
  const settingsCache = deps.settingsCache || require('../../../shared/settings-cache');
248
248
 
249
- const labs = deps.labs || require('../../../shared/labs');
250
249
  const webAnalyticsEnabled = settingsCache.get('web_analytics_enabled');
251
250
  const TinybirdServiceWrapper = require('../tinybird');
252
251
 
253
- if (
254
- webAnalyticsEnabled ||
255
- (labs.isSet('automationsTinybirdSync') && config.get('tinybird:stats'))
256
- ) {
252
+ if (webAnalyticsEnabled || config.get('tinybird:stats')) {
257
253
  TinybirdServiceWrapper.init();
258
254
  }
259
255
 
@@ -8,15 +8,12 @@ const bson_objectid_1 = __importDefault(require("bson-objectid"));
8
8
  const logging_1 = __importDefault(require("@tryghost/logging"));
9
9
  const config_1 = __importDefault(require("../../../shared/config"));
10
10
  // @ts-expect-error This module lacks type definitions.
11
- const labs_1 = __importDefault(require("../../../shared/labs"));
12
- // @ts-expect-error This module lacks type definitions.
13
11
  const settings_cache_1 = __importDefault(require("../../../shared/settings-cache"));
14
12
  const db_1 = require("../../data/db");
15
13
  const tinybird_sync_service_1 = require("./tinybird-sync-service");
16
14
  const service = (0, tinybird_sync_service_1.createTinybirdSyncService)({
17
15
  config: config_1.default,
18
16
  settingsCache: settings_cache_1.default,
19
- labs: labs_1.default,
20
17
  knex: db_1.knex,
21
18
  logging: logging_1.default,
22
19
  random: Math.random,
@@ -15,7 +15,7 @@ const BATCH_SIZE = 5000;
15
15
  const MAX_PAYLOAD_BYTES = 9 * 1024 * 1024;
16
16
  const MAX_PAYLOAD_MESSAGES = 1000;
17
17
  const REQUEST_TIMEOUT_MS = 5 * 60 * 1000;
18
- function createTinybirdSyncService({ config, settingsCache, labs, knex, logging, random, now, fetch, createId, }) {
18
+ function createTinybirdSyncService({ config, settingsCache, knex, logging, random, now, fetch, createId, }) {
19
19
  let scheduled = false;
20
20
  const syncAll = async (ingest) => {
21
21
  const results = await Promise.allSettled(sync_table_to_tinybird_1.AUTOMATION_SYNC_TARGETS.map(async (target) => {
@@ -39,9 +39,6 @@ function createTinybirdSyncService({ config, settingsCache, labs, knex, logging,
39
39
  }
40
40
  };
41
41
  const sync = async () => {
42
- if (!labs.isSet('automationsTinybirdSync')) {
43
- return;
44
- }
45
42
  const ingest = (0, get_ingest_config_1.getIngestConfig)({ config, settingsCache });
46
43
  if (!ingest) {
47
44
  return;
@@ -59,8 +56,7 @@ function createTinybirdSyncService({ config, settingsCache, labs, knex, logging,
59
56
  return;
60
57
  }
61
58
  scheduled = true;
62
- const isSyncEnabled = labs.isSet('automationsTinybirdSync');
63
- logging.info({ system: { event: 'tinybird.sync.started' } }, `[Tinybird sync] Started: sync ${isSyncEnabled ? 'enabled' : 'disabled'} by labs flag (but may change)`);
59
+ logging.info({ system: { event: 'tinybird.sync.started' } }, '[Tinybird sync] Started');
64
60
  const at = (0, cron_1.randomFiveMinuteCron)(random);
65
61
  logging.info(`[Background Job] ${tinybird_sync_job_1.default.type} scheduled at ${at}`);
66
62
  try {
@@ -1,3 +1,4 @@
1
+ const errors = require('@tryghost/errors');
1
2
  const express = require('../../../../../shared/express');
2
3
  const api = require('../../../../api').endpoints;
3
4
  const { http } = require('@tryghost/api-framework');
@@ -6,6 +7,7 @@ const apiMw = require('../../middleware');
6
7
  const mw = require('./middleware');
7
8
  const labs = require('../../../../../shared/labs');
8
9
  const limits = require('../../../../services/limits');
10
+ const appInstallations = require('../../../../services/app-installations');
9
11
 
10
12
  const shared = require('../../../shared');
11
13
 
@@ -232,6 +234,27 @@ module.exports = function apiRoutes() {
232
234
  metafieldsRouter.put('/:namespace/:key', http(api.membersMetafields.edit));
233
235
  metafieldsRouter.delete('/:namespace/:key', http(api.membersMetafields.destroy));
234
236
 
237
+ // ## Apps
238
+ // Everything about apps sits behind the private `apps` flag, asked once here so a route
239
+ // added below is guarded by being there. The table is still in development, so it only
240
+ // exists in development and testing databases: anywhere else the routes answer as if
241
+ // the flag were off, whatever the flag says.
242
+ //
243
+ // Authenticated as a route here rather than inside the mount, for the same reason as
244
+ // the members metafields router above.
245
+ const appsRouter = express.Router('admin api apps');
246
+ router.all(['/apps', '/apps/*'], mw.authAdminApi);
247
+ router.use('/apps', appsRouter);
248
+ appsRouter.use(labs.enabledMiddleware('apps'));
249
+ // Answers as if apps did not exist wherever the table does not.
250
+ appsRouter.use((req, res, next) => {
251
+ next(appInstallations.isAvailable() ? undefined : new errors.NotFoundError());
252
+ });
253
+
254
+ appsRouter.get('/installations', http(api.appInstallations.browse));
255
+ appsRouter.get('/installations/:id', http(api.appInstallations.read));
256
+ appsRouter.delete('/installations/:id', http(api.appInstallations.destroy));
257
+
235
258
  router.get('/members/:id', mw.authAdminApi, http(api.members.read));
236
259
  router.put('/members/:id', mw.authAdminApi, http(api.members.edit));
237
260
  router.delete('/members/:id', mw.authAdminApi, http(api.members.destroy));
@@ -396,18 +419,8 @@ module.exports = function apiRoutes() {
396
419
  router.post('/db/media/inline', mw.authAdminApi, http(api.db.inlineMedia));
397
420
 
398
421
  // ## Exports
399
- router.get(
400
- '/exports/download',
401
- mw.authAdminApi,
402
- labs.enabledMiddleware('selfServeArchives'),
403
- http(api.exports.download),
404
- );
405
- router.post(
406
- '/exports',
407
- mw.authAdminApi,
408
- labs.enabledMiddleware('selfServeArchives'),
409
- http(api.exports.add),
410
- );
422
+ router.get('/exports/download', mw.authAdminApi, http(api.exports.download));
423
+ router.post('/exports', mw.authAdminApi, http(api.exports.add));
411
424
 
412
425
  // ## Slack
413
426
  router.post('/slack/test', mw.authAdminApi, http(api.slack.sendTest));
@@ -27,15 +27,16 @@ const messages = {
27
27
  };
28
28
 
29
29
  // flags in this list always return `true`, allows quick global enable prior to full flag removal
30
- // Admin no longer reads admin7Pill, postsListReact or membersActivityReact; they stay enabled
31
- // for older Admin builds during independent deployments.
30
+ // Admin no longer reads admin7Pill, postsListReact, membersActivityReact or selfServeArchives;
31
+ // they stay enabled for older Admin builds during independent deployments.
32
32
  const GA_FEATURES = [
33
33
  'automationAnalytics',
34
+ 'automationRunAnalytics',
34
35
  'admin7Pill',
35
36
  'globalSearchReact',
36
37
  'postsListReact',
37
38
  'membersActivityReact',
38
- 'dunningWarnings',
39
+ 'selfServeArchives',
39
40
  ];
40
41
 
41
42
  // These features are considered publicly available and can be enabled/disabled by users
@@ -51,8 +52,6 @@ const PUBLIC_BETA_FEATURES = [
51
52
  const PRIVATE_FEATURES = [
52
53
  'automations',
53
54
  'automationsPerTier',
54
- 'automationRunAnalytics',
55
- 'automationsTinybirdSync',
56
55
  'stripeAutomaticTax',
57
56
  'importMemberTier',
58
57
  'csvContentImporter',
@@ -64,7 +63,6 @@ const PRIVATE_FEATURES = [
64
63
  'membersCustomFields',
65
64
  'stripeCheckoutCollection',
66
65
  'paywallImprovements',
67
- 'selfServeArchives',
68
66
  'machinePayments',
69
67
  'editorReact',
70
68
  'authReact',