ghost 6.56.0 → 6.57.1

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 (428) hide show
  1. package/bin/validate-adapters.js +74 -0
  2. package/bin/validate-adapters.ts +87 -0
  3. package/bin/warm-compile-cache.sh +60 -0
  4. package/components/tryghost-adapter-base-scheduling-0.2.1.tgz +0 -0
  5. package/components/tryghost-custom-field-types-0.0.0.tgz +0 -0
  6. package/components/tryghost-kg-converters-1.2.4.tgz +0 -0
  7. package/components/tryghost-kg-default-cards-10.3.4.tgz +0 -0
  8. package/components/tryghost-kg-default-nodes-2.1.5.tgz +0 -0
  9. package/components/tryghost-kg-default-transforms-1.3.3.tgz +0 -0
  10. package/components/tryghost-kg-html-to-lexical-1.3.3.tgz +0 -0
  11. package/components/tryghost-kg-lexical-html-renderer-1.4.3.tgz +0 -0
  12. package/components/tryghost-parse-email-address-0.0.0.tgz +0 -0
  13. package/core/app.js +1 -1
  14. package/core/boot.js +21 -25
  15. package/core/bridge.js +10 -12
  16. package/core/built/admin/assets/Combination-BDvCdg1t.js +41 -0
  17. package/core/built/admin/assets/about-BNojRaxf.js +1 -0
  18. package/core/built/admin/assets/{account-migration-D3O_yVck.js → account-migration-DMS8JsM-.js} +1 -1
  19. package/core/built/admin/assets/action-list-lGzZLHc8.js +1 -0
  20. package/core/built/admin/assets/activitypub/{account-migration-D8FbP-Z-.js → account-migration-DHFbePl6.js} +7 -7
  21. package/core/built/admin/assets/activitypub/activitypub.js +3 -3
  22. package/core/built/admin/assets/activitypub/{avatar-11EA1VSs.js → avatar-CT4tS2_S.js} +2 -2
  23. package/core/built/admin/assets/activitypub/{avatar-flipboard-jfbLG-xe.js → avatar-flipboard-ByYWKipo.js} +2 -2
  24. package/core/built/admin/assets/activitypub/{bluesky-sharing-f1jTQe2P.js → bluesky-sharing-srVnSHDz.js} +8 -8
  25. package/core/built/admin/assets/activitypub/{content-formatters-nmTOPY9M.js → content-formatters-C2sos5Ht.js} +3 -3
  26. package/core/built/admin/assets/activitypub/{copy-BCwo9ep3.js → copy-B1UBTOoZ.js} +2 -2
  27. package/core/built/admin/assets/activitypub/{dist-BEfX-0aW.js → dist-MPsb2WZ1.js} +4 -4
  28. package/core/built/admin/assets/activitypub/{domain-AaDlZlrA.js → domain-DzcM9Dzo.js} +9 -9
  29. package/core/built/admin/assets/activitypub/{edit-profile-DPi8Tl5M.js → edit-profile-CsQ_4xAk.js} +5 -5
  30. package/core/built/admin/assets/activitypub/{explore-CxTzjILQ.js → explore-CJe0DZBK.js} +8 -8
  31. package/core/built/admin/assets/activitypub/{feed-DPv5x2MX.js → feed-CKV3qWON.js} +9 -9
  32. package/core/built/admin/assets/activitypub/{field-DbgA8lat.js → field-DckHfDJf.js} +3 -3
  33. package/core/built/admin/assets/activitypub/{hash-BWuZoYkA.js → hash-Dlp8RM55.js} +2 -2
  34. package/core/built/admin/assets/activitypub/{inbox-35VDQ2K2.js → inbox-CsPy4R0M.js} +2 -2
  35. package/core/built/admin/assets/activitypub/{inbox-DSrF_i1O.js → inbox-DAsjzUUe.js} +10 -10
  36. package/core/built/admin/assets/activitypub/{label-CPjFBbo7.js → label-DWyo9E5T.js} +3 -3
  37. package/core/built/admin/assets/activitypub/layout-DM_3pqsU.js +1 -0
  38. package/core/built/admin/assets/activitypub/{moderation-BkDC1XVZ.js → moderation-B0Inc03M.js} +7 -7
  39. package/core/built/admin/assets/activitypub/{note-xEyH4xT-.js → note-CSE54lER.js} +7 -7
  40. package/core/built/admin/assets/activitypub/{notifications-DClaL3ui.js → notifications-RSwbKqdh.js} +9 -9
  41. package/core/built/admin/assets/activitypub/{preferences-DpnOa-4Z.js → preferences-CE231n2_.js} +11 -11
  42. package/core/built/admin/assets/activitypub/{profile-Dvi9mz-6.js → profile-aaQ1j1Eo.js} +13 -13
  43. package/core/built/admin/assets/activitypub/{reply-Cs9ySd6Z.js → reply-Cjb0Vs3m.js} +2 -2
  44. package/core/built/admin/assets/activitypub/{routes-DFBeTnfn.js → routes-Djb0mc9k.js} +26 -26
  45. package/core/built/admin/assets/activitypub/{separator-C5u_VjzY.js → separator-D8pROI5n.js} +3 -3
  46. package/core/built/admin/assets/activitypub/{settings-CV2fFu6D.js → settings-DMc32IQl.js} +5 -5
  47. package/core/built/admin/assets/activitypub/{site-_TgDd_EM.js → site-DVIcsohX.js} +4 -4
  48. package/core/built/admin/assets/activitypub/{step-1-BfnNL2oF.js → step-1-9Yey9usF.js} +5 -5
  49. package/core/built/admin/assets/activitypub/{step-2-DLndZkO-.js → step-2-DdYmKRXV.js} +6 -6
  50. package/core/built/admin/assets/activitypub/{step-3-A-PZTtRE.js → step-3-hdSkcjZU.js} +10 -10
  51. package/core/built/admin/assets/activitypub/{tabs-BUbz7tea.js → tabs-CUqseYzQ.js} +6 -6
  52. package/core/built/admin/assets/activitypub/{topic-filter-BdX_xYYT.js → topic-filter-Coni6OXM.js} +3 -3
  53. package/core/built/admin/assets/activitypub/{use-activity-pub-queries-EwuRemHh.js → use-activity-pub-queries-MeHTPMQv.js} +2 -2
  54. package/core/built/admin/assets/activitypub/{use-navigate-with-base-path-cy4ACBc4.js → use-navigate-with-base-path-B046AM38.js} +24 -7
  55. package/core/built/admin/assets/activitypub/{use-reply-chain-data-CyFcGfan.js → use-reply-chain-data-1AO-Xob6.js} +5 -5
  56. package/core/built/admin/assets/activitypub/{x-7zk8mV2p.js → x-BRfRYd16.js} +2 -2
  57. package/core/built/admin/assets/add-integration-modal-DU9ghGLS.js +1 -0
  58. package/core/built/admin/assets/add-newsletter-modal-DFxuQrYu.js +1 -0
  59. package/core/built/admin/assets/add-recommendation-modal-D-3LOrdd.js +1 -0
  60. package/core/built/admin/assets/analytics-DTIMRhWw.js +1 -0
  61. package/core/built/admin/assets/announcement-bar-modal-DGCcezB6.js +1 -0
  62. package/core/built/admin/assets/api-keys-Dx4NeNpK.js +1 -0
  63. package/core/built/admin/assets/api-keys-_iCT4dxr.js +1 -0
  64. package/core/built/admin/assets/{audience-CkBb2LlO.js → audience-DkknNqhZ.js} +1 -1
  65. package/core/built/admin/assets/automated-emails-PayPCT8L.js +1 -0
  66. package/core/built/admin/assets/{automations-DhKeS_U5.js → automations-162q5HjL.js} +1 -1
  67. package/core/built/admin/assets/{automations-cMllU545.js → automations-DvSU7QhW.js} +1 -1
  68. package/core/built/admin/assets/{bluesky-sharing-DPGpyD-G.js → bluesky-sharing-yZ9A_jL8.js} +1 -1
  69. package/core/built/admin/assets/brand-icon-09YqFZjV.js +1 -0
  70. package/core/built/admin/assets/{breadcrumb-DIfcF5ow.js → breadcrumb-CjOH7LCU.js} +1 -1
  71. package/core/built/admin/assets/{chart-helpers-DV0FkdcD.js → chart-helpers-DGxvWnBT.js} +1 -1
  72. package/core/built/admin/assets/checkbox-BSUcnNq_.js +1 -0
  73. package/core/built/admin/assets/{chunk.332.05991a101d67f732dec3.js → chunk.424.4a27628efdade15c2dab.js} +28 -27
  74. package/core/built/admin/assets/chunk.524.dce1c51e0bc30fb2718e.js +25 -0
  75. package/core/built/admin/assets/{chunk.211.6f7328d63c41536451b3.js → chunk.571.40f093ed6d883993714e.js} +5 -5
  76. package/core/built/admin/assets/chunk.582.3a430d5c6a744fd475b6.js +27 -0
  77. package/core/built/admin/assets/code-editor-view-BcyKABrG.js +1 -0
  78. package/core/built/admin/assets/color-picker-field-8NoSnhNI.js +1 -0
  79. package/core/built/admin/assets/color-utils-Dzeeuj1F.js +1 -0
  80. package/core/built/admin/assets/combobox-5rdgDrDV.js +1 -0
  81. package/core/built/admin/assets/command-BOYMbZa_.js +1 -0
  82. package/core/built/admin/assets/comments-BhNM5CYl.js +1 -0
  83. package/core/built/admin/assets/components-CwL6a6As-B0E0-NBJ.js +1 -0
  84. package/core/built/admin/assets/confirmation-modal-CYD7x3UQ.js +1 -0
  85. package/core/built/admin/assets/content-api-modal-BtW4Z1-1.js +1 -0
  86. package/core/built/admin/assets/copy-field-CEp_XieW.js +1 -0
  87. package/core/built/admin/assets/currency-Dy4Tc2R2.js +1 -0
  88. package/core/built/admin/assets/custom-integration-modal-bk3lNEuQ.js +1 -0
  89. package/core/built/admin/assets/{date-range-select-dXc-GsVe.js → date-range-select-B3Ij1Rqg.js} +1 -1
  90. package/core/built/admin/assets/debounce-Cx5B_9OI.js +3 -0
  91. package/core/built/admin/assets/design-and-theme-modal-B2xYKS1I.js +23 -0
  92. package/core/built/admin/assets/dirty-confirm-dialog-YE9L21gY.js +1 -0
  93. package/core/built/admin/assets/disabled-sources-indicator-uIzbWL7K.js +1 -0
  94. package/core/built/admin/assets/{dist-pHiOxyxa.js → dist-ARN6XTcR.js} +1 -1
  95. package/core/built/admin/assets/dist-B2PvRwLn.js +1 -0
  96. package/core/built/admin/assets/{dist-DyViJwIy.js → dist-Bg8bSvLA.js} +3 -3
  97. package/core/built/admin/assets/dist-CsvXCJSe.js +23 -0
  98. package/core/built/admin/assets/dist-DlCyLrOC.js +1 -0
  99. package/core/built/admin/assets/dist-j1xxxnY8.js +1 -0
  100. package/core/built/admin/assets/dist-myQaY0ka.js +1 -0
  101. package/core/built/admin/assets/{domain-B5ogXUBa.js → domain-DzrOAPY8.js} +1 -1
  102. package/core/built/admin/assets/{dropzone-BnFG7dDP.js → dropzone-CzhP33wa.js} +1 -1
  103. package/core/built/admin/assets/{edit-profile-B07jCZy9.js → edit-profile-oufs4R4n.js} +1 -1
  104. package/core/built/admin/assets/editor-BBKrunMK.js +8 -0
  105. package/core/built/admin/assets/embed-signup-form-modal-Ch3s6lqA.js +9 -0
  106. package/core/built/admin/assets/{explore-Ca1y8HW_.js → explore-CA-R01Br.js} +1 -1
  107. package/core/built/admin/assets/{feed-x4iFMBP4.js → feed-ClC8_Gld.js} +1 -1
  108. package/core/built/admin/assets/{field-ChCAabZg.js → field-B3OPenbP.js} +1 -1
  109. package/core/built/admin/assets/{filter-sources-LdtH1e-T.js → filter-sources-CiPLnG9A.js} +1 -1
  110. package/core/built/admin/assets/filters-Bz0HsRie.js +1 -0
  111. package/core/built/admin/assets/{filters-CjVKRP8U.js → filters-yFvybRMe.js} +1 -1
  112. package/core/built/admin/assets/first-promoter-modal-BC0UpYCD.js +1 -0
  113. package/core/built/admin/assets/format-url-C-t8G6Qd.js +1 -0
  114. package/core/built/admin/assets/{ghost-7f01131115186d1af84ead76e79646dd.js → ghost-d683fbeb945b13155ea669caa5d8db41.js} +300 -298
  115. package/core/built/admin/assets/{gift-link-modal-Buqh2vXh.js → gift-link-modal-BKRuqo-k.js} +1 -1
  116. package/core/built/admin/assets/{gift-links-CsUhefWG.js → gift-links-CVuqLhsZ.js} +1 -1
  117. package/core/built/admin/assets/global-data-provider-RzW6Bnlf.js +1 -0
  118. package/core/built/admin/assets/grid-CUciBfjf.js +1 -0
  119. package/core/built/admin/assets/{growth-DajOBdN0.js → growth-ClNAldfL.js} +1 -1
  120. package/core/built/admin/assets/{growth-Dy2UOSay.js → growth-Dz8OSG06.js} +2 -2
  121. package/core/built/admin/assets/history-modal-DIVuQ5Ct.js +1 -0
  122. package/core/built/admin/assets/{hooks-CixO01lL.js → hooks-D6EE7BIt.js} +2 -2
  123. package/core/built/admin/assets/html-field-CVZ2sMxw.js +1 -0
  124. package/core/built/admin/assets/iframe-buffering-e8vH4-Qn.js +1 -0
  125. package/core/built/admin/assets/images-Ok8FFJ3A.js +1 -0
  126. package/core/built/admin/assets/{inbox-BUOLDoji.js → inbox-CMUVI6qb.js} +13 -13
  127. package/core/built/admin/assets/index-DUXM0NlS.js +67 -0
  128. package/core/built/admin/assets/index-Ds2iUoF6.css +1 -0
  129. package/core/built/admin/assets/index.es-B9tvf-t4.js +1 -0
  130. package/core/built/admin/assets/{input-group-uSK3XT7b.js → input-group-2XU-iuh_.js} +1 -1
  131. package/core/built/admin/assets/integration-header-B4DyXV06.js +1 -0
  132. package/core/built/admin/assets/integrations-BT6_iJvu.js +1 -0
  133. package/core/built/admin/assets/invite-user-modal-DrZ7xxrZ.js +1 -0
  134. package/core/built/admin/assets/isEmail-BOfC647y.js +1 -0
  135. package/core/built/admin/assets/koenig-editor-base-BPj4S760.js +1 -0
  136. package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +9254 -9251
  137. package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +124 -124
  138. package/core/built/admin/assets/{koenig-lexical-BFc4yFed.js → koenig-lexical-Cdjv-m3_.js} +98 -98
  139. package/core/built/admin/assets/{kpi-card-CCt-qeWD.js → kpi-card-J8Mj1sDX.js} +1 -1
  140. package/core/built/admin/assets/{kpi-card-BN-E6WYX.js → kpi-card-awos0BMH.js} +1 -1
  141. package/core/built/admin/assets/{kpi-helpers-DlxTbTs4.js → kpi-helpers-SEhvH-8E.js} +1 -1
  142. package/core/built/admin/assets/{kpi-tabs-BlcUttpn.js → kpi-tabs-R6BjsKhd.js} +1 -1
  143. package/core/built/admin/assets/{label-AVrxwmsz.js → label-BMC3dzLI.js} +1 -1
  144. package/core/built/admin/assets/label-picker-Bnlru5tO.js +1 -0
  145. package/core/built/admin/assets/{labels-DAIqKed7.js → labels-DAPjKPy2.js} +1 -1
  146. package/core/built/admin/assets/layout-yR9rlvEw.js +1 -0
  147. package/core/built/admin/assets/limit-modal-COmLevuY.js +1 -0
  148. package/core/built/admin/assets/{link-helpers-D9LsDOkG.js → link-helpers-Bzkk68GD.js} +1 -1
  149. package/core/built/admin/assets/member-custom-fields-BvFKEpxJ.js +1 -0
  150. package/core/built/admin/assets/member-detail-CWdPAe3p.js +1 -0
  151. package/core/built/admin/assets/members-Bnt0ih2R.js +11 -0
  152. package/core/built/admin/assets/modal-page-BMTzFHVO.js +1 -0
  153. package/core/built/admin/assets/{moderation-CQ3urTFE.js → moderation-mPjYHgZW.js} +1 -1
  154. package/core/built/admin/assets/multi-select-combobox-CsoyHAiX.js +1 -0
  155. package/core/built/admin/assets/navigation-modal-D_c0badj.js +1 -0
  156. package/core/built/admin/assets/newsletter-DR49NyGS.js +1 -0
  157. package/core/built/admin/assets/newsletter-detail-modal-B45pL1pb.js +1 -0
  158. package/core/built/admin/assets/newsletter-emails-B-OLfrvr.js +1 -0
  159. package/core/built/admin/assets/{newsletters-vt3Ni1Ov.js → newsletters-DGs5BaKH.js} +1 -1
  160. package/core/built/admin/assets/{newsletters-BXJbJgA-.js → newsletters-Du0lzskS.js} +1 -1
  161. package/core/built/admin/assets/{note-Dk877BTv.js → note-CSVsUKBm.js} +1 -1
  162. package/core/built/admin/assets/{notifications-CPlLjPxO.js → notifications-DV9FRtDw.js} +1 -1
  163. package/core/built/admin/assets/{offers-DEniVRD5.js → offers-Um6ab71O.js} +1 -1
  164. package/core/built/admin/assets/offers-container-modal-B0hFS8nk.js +1 -0
  165. package/core/built/admin/assets/{onboarding-route-DdStOtjy.js → onboarding-route--jL5KLPD.js} +1 -1
  166. package/core/built/admin/assets/overview-B-ZPNqMo.js +1 -0
  167. package/core/built/admin/assets/overview-D96iwoJi.js +1 -0
  168. package/core/built/admin/assets/{page-header-pp4dWmOS.js → page-header-DAvDYKO9.js} +1 -1
  169. package/core/built/admin/assets/{pages-B4goRPUn.js → pages-BVMnpI2O.js} +1 -1
  170. package/core/built/admin/assets/pintura-modal-jiUCxX0Q.js +1 -0
  171. package/core/built/admin/assets/portal-frame-D4D4nEO6.js +1 -0
  172. package/core/built/admin/assets/portal-modal-BLFdYeDH.js +1 -0
  173. package/core/built/admin/assets/{post-analytics-BMJrI81R.js → post-analytics-BhAtiFJp.js} +1 -1
  174. package/core/built/admin/assets/post-analytics-header-DK27VjJm.js +1 -0
  175. package/core/built/admin/assets/{post-analytics-provider-Btml5vJI.js → post-analytics-provider-DGg0FUqs.js} +1 -1
  176. package/core/built/admin/assets/{post-share-modal-AcUZkbfL.js → post-share-modal-DtFAC132.js} +1 -1
  177. package/core/built/admin/assets/{posts-DhXE7b6r.js → posts-CxG7v0-M.js} +1 -1
  178. package/core/built/admin/assets/preferences-CuWLISHa.js +5 -0
  179. package/core/built/admin/assets/preview-chrome-BOVO6_PE.js +1 -0
  180. package/core/built/admin/assets/preview-modal-DctYx20m.js +1 -0
  181. package/core/built/admin/assets/profile-BsKYYtuL.js +1 -0
  182. package/core/built/admin/assets/radio-group-CI5_Kf_d.js +1 -0
  183. package/core/built/admin/assets/{referrers-pmJT4k3-.js → referrers-B9QDAfYm.js} +1 -1
  184. package/core/built/admin/assets/{router-provider-BmEZf6c6.js → router-provider-DSQfprxH.js} +1 -1
  185. package/core/built/admin/assets/select-CkXqNJDJ.js +1 -0
  186. package/core/built/admin/assets/settings-B4vM-rgV.js +4 -0
  187. package/core/built/admin/assets/{settings-BGgk7oCI.js → settings-ZCj-W56o.js} +1 -1
  188. package/core/built/admin/assets/settings-breadcrumbs-DIq31FwA.js +1 -0
  189. package/core/built/admin/assets/{share-modal-BeuydaBf.js → share-modal-BNj14g79.js} +1 -1
  190. package/core/built/admin/assets/slack-modal-C6SpMQ-q.js +1 -0
  191. package/core/built/admin/assets/social-urls-CHQ9cTAK.js +1 -0
  192. package/core/built/admin/assets/{sort-button-Df4t2AL8.js → sort-button-B-k54ow2.js} +1 -1
  193. package/core/built/admin/assets/{source-icon-ajoaKb41.js → source-icon-Cej3sZM6.js} +1 -1
  194. package/core/built/admin/assets/{stats-C_lfxZAw.js → stats-B2KW8E2y.js} +1 -1
  195. package/core/built/admin/assets/{stats-view-Cxot1iD8.js → stats-view-CVzU2-p5.js} +1 -1
  196. package/core/built/admin/assets/{step-1-ogAA4lDr.js → step-1-Dbn1iIMU.js} +1 -1
  197. package/core/built/admin/assets/{step-2-C5Xini-7.js → step-2-BahnSuN6.js} +1 -1
  198. package/core/built/admin/assets/{step-3-CZv1FV81.js → step-3-CW04e2ZJ.js} +2 -2
  199. package/core/built/admin/assets/stripe-button-BkBZqaGG.js +1 -0
  200. package/core/built/admin/assets/stripe-connect-modal-CYDDrSJm.js +1 -0
  201. package/core/built/admin/assets/switch-CE2NuMn-.js +1 -0
  202. package/core/built/admin/assets/{table-DFj1wiYv.js → table-BtLcEi1M.js} +1 -1
  203. package/core/built/admin/assets/tabs-BaWRfg8j.js +1 -0
  204. package/core/built/admin/assets/tag-detail-CmqiC32L.js +1 -0
  205. package/core/built/admin/assets/tags-BbygUvNB.js +1 -0
  206. package/core/built/admin/assets/tags-BwCgmr5v.js +1 -0
  207. package/core/built/admin/assets/testimonials-modal--bdzppyh.js +1 -0
  208. package/core/built/admin/assets/tier-detail-modal-BZmAsIy4.js +1 -0
  209. package/core/built/admin/assets/{tiers-DYsa4DCo.js → tiers-Da6kxHQP.js} +1 -1
  210. package/core/built/admin/assets/toggle-group-CVl-Xigr.js +1 -0
  211. package/core/built/admin/assets/{topic-filter-DGDxDalo.js → topic-filter-BoRHRbL1.js} +1 -1
  212. package/core/built/admin/assets/transistor-modal-CBPDqEGy.js +1 -0
  213. package/core/built/admin/assets/unsplash-modal-CixoRAO2.js +1 -0
  214. package/core/built/admin/assets/{use-analytics-data-C76v4LIp.js → use-analytics-data-BiWvcf1u.js} +1 -1
  215. package/core/built/admin/assets/use-check-theme-limit-error-B14lZukR.js +2 -0
  216. package/core/built/admin/assets/use-feature-flag-BmJoc5WN.js +1 -0
  217. package/core/built/admin/assets/use-filter-params-ChoV72Hw.js +1 -0
  218. package/core/built/admin/assets/use-filterable-api-iMpDZBxD.js +1 -0
  219. package/core/built/admin/assets/use-form-vRjI0Kwk.js +1 -0
  220. package/core/built/admin/assets/{use-growth-stats-aNXJOmQr.js → use-growth-stats-Dom8xVzo.js} +1 -1
  221. package/core/built/admin/assets/use-hash-link-navigation-guard-B9j6HIrV.js +1 -0
  222. package/core/built/admin/assets/use-limiter-Dwg8ivgQ.js +2 -0
  223. package/core/built/admin/assets/use-pintura-config-Bj4QmiVk.js +1 -0
  224. package/core/built/admin/assets/use-pintura-editor-DvrslmPE.js +1 -0
  225. package/core/built/admin/assets/{use-post-referrers-CG0SZioj.js → use-post-referrers-pxIHTXFs.js} +1 -1
  226. package/core/built/admin/assets/use-query-params-DHhhu8wi.js +1 -0
  227. package/core/built/admin/assets/{use-reply-chain-data-RLdPKRmS.js → use-reply-chain-data-CmFe0N6F.js} +1 -1
  228. package/core/built/admin/assets/use-setting-group-Cmp_sy-d.js +1 -0
  229. package/core/built/admin/assets/use-settings-navigation-BmROPrls.js +1 -0
  230. package/core/built/admin/assets/{use-simple-pagination-DgcF69KI.js → use-simple-pagination-D5rL3YwO.js} +1 -1
  231. package/core/built/admin/assets/{use-tinybird-query-C1R_Kbmy.js → use-tinybird-query-DP2dMaq7.js} +1 -1
  232. package/core/built/admin/assets/{use-tinybird-token-C9fPjVM_.js → use-tinybird-token-DrK_skPQ.js} +1 -1
  233. package/core/built/admin/assets/use-url-input-ChrUZBFf.js +5 -0
  234. package/core/built/admin/assets/user-detail-modal-XODpOqNC.js +1 -0
  235. package/core/built/admin/assets/{users-Dkv8NQZe.js → users-B6hIds6n.js} +1 -1
  236. package/core/built/admin/assets/validator-Cr0sMKuW.js +7 -0
  237. package/core/built/admin/assets/{virtual-list-BmTUzeoF.js → virtual-list-Dnoafqbk.js} +1 -1
  238. package/core/built/admin/assets/web-D71mjB2r.js +1 -0
  239. package/core/built/admin/assets/web-z71lA1Y3.js +2 -0
  240. package/core/built/admin/assets/zapier-modal-Bp507Um7.js +1 -0
  241. package/core/built/admin/index.html +16 -14
  242. package/core/frontend/helpers/authors.js +1 -1
  243. package/core/frontend/helpers/link.js +3 -8
  244. package/core/frontend/helpers/tags.js +1 -1
  245. package/core/frontend/meta/author-url.js +2 -2
  246. package/core/frontend/meta/url.js +4 -4
  247. package/core/frontend/services/proxy.js +1 -1
  248. package/core/frontend/services/routing/events.js +20 -6
  249. package/core/frontend/services/routing/permalink-adapter.js +0 -13
  250. package/core/frontend/services/routing/permalink-adapter.ts +0 -13
  251. package/core/frontend/services/routing/router-manager.js +15 -9
  252. package/core/frontend/services/sitemap/base-site-map-generator.js +1 -28
  253. package/core/frontend/services/sitemap/site-map-manager.js +22 -57
  254. package/core/frontend/web/middleware/admin-toolbar.js +1 -1
  255. package/core/frontend/web/middleware/serve-indexnow-key.js +1 -7
  256. package/core/server/adapters/route-settings/utils.js +10 -2
  257. package/core/server/adapters/route-settings/utils.ts +12 -2
  258. package/core/server/api/endpoints/email-post.js +1 -1
  259. package/core/server/api/endpoints/index.js +2 -2
  260. package/core/server/api/endpoints/previews.js +1 -1
  261. package/core/server/api/endpoints/search-index-public.js +3 -3
  262. package/core/server/api/endpoints/search-index.js +4 -4
  263. package/core/server/api/endpoints/themes.js +6 -1
  264. package/core/server/api/endpoints/utils/serializers/input/authors.js +2 -2
  265. package/core/server/api/endpoints/utils/serializers/input/comments.js +1 -1
  266. package/core/server/api/endpoints/utils/serializers/input/pages.js +3 -3
  267. package/core/server/api/endpoints/utils/serializers/input/posts.js +3 -3
  268. package/core/server/api/endpoints/utils/serializers/input/tags.js +1 -1
  269. package/core/server/api/endpoints/utils/serializers/input/users.js +2 -2
  270. package/core/server/api/endpoints/utils/serializers/input/utils/url.js +31 -13
  271. package/core/server/api/endpoints/utils/serializers/output/mappers/posts.js +2 -2
  272. package/core/server/api/endpoints/utils/serializers/output/mappers/tags.js +2 -2
  273. package/core/server/api/endpoints/utils/serializers/output/mappers/users.js +8 -0
  274. package/core/server/api/endpoints/utils/serializers/output/utils/url.js +18 -11
  275. package/core/server/data/migrations/versions/6.57/2026-08-04-18-00-00-store-custom-field-values-as-leaf-rows.js +74 -0
  276. package/core/server/data/migrations/versions/6.57/2026-08-04-18-54-14-reset-automation-email-analytics.js +25 -0
  277. package/core/server/data/schema/commands.js +21 -5
  278. package/core/server/data/schema/schema.js +24 -5
  279. package/core/server/lib/common/to-plain.ts +1 -1
  280. package/core/server/models/base/plugins/overrides.js +7 -5
  281. package/core/server/models/post.js +2 -2
  282. package/core/server/services/adapter-manager/adapter-manager.js +1 -45
  283. package/core/server/services/adapter-manager/adapter-manager.ts +2 -48
  284. package/core/server/services/adapter-manager/adapter-paths.js +28 -0
  285. package/core/server/services/adapter-manager/adapter-paths.ts +26 -0
  286. package/core/server/services/adapter-manager/base-classes.js +22 -0
  287. package/core/server/services/adapter-manager/base-classes.ts +22 -0
  288. package/core/server/services/adapter-manager/index.js +4 -27
  289. package/core/server/services/adapter-manager/index.ts +4 -31
  290. package/core/server/services/adapter-manager/utils.js +50 -0
  291. package/core/server/services/adapter-manager/utils.ts +57 -0
  292. package/core/server/services/audience-feedback/audience-feedback-controller.js +11 -2
  293. package/core/server/services/audience-feedback/audience-feedback-service.js +2 -2
  294. package/core/server/services/audience-feedback/feedback-repository.js +4 -1
  295. package/core/server/services/auth/session/session-service.js +18 -3
  296. package/core/server/services/automations/database-automations-repository.js +5 -5
  297. package/core/server/services/automations/database-automations-repository.ts +12 -12
  298. package/core/server/services/automations/database-date.js +52 -3
  299. package/core/server/services/automations/database-date.ts +23 -2
  300. package/core/server/services/automations/service.js +2 -2
  301. package/core/server/services/automations/service.ts +2 -2
  302. package/core/server/services/comments/comments-service-emails.js +1 -1
  303. package/core/server/services/comments/comments-service.js +1 -1
  304. package/core/server/services/email-analytics/email-analytics-service-wrapper.js +12 -3
  305. package/core/server/services/email-analytics/email-analytics-service.js +4 -3
  306. package/core/server/services/email-analytics/index.js +2 -3
  307. package/core/server/services/email-analytics/index.ts +1 -2
  308. package/core/server/services/email-analytics/lib/queries.js +91 -124
  309. package/core/server/services/email-analytics/lib/queries.ts +341 -0
  310. package/core/server/services/email-service/email-service-wrapper.js +2 -1
  311. package/core/server/services/email-service/mailgun-email-provider.js +15 -0
  312. package/core/server/services/indexnow-ping/index.js +0 -2
  313. package/core/server/services/indexnow-ping/index.ts +0 -2
  314. package/core/server/services/indexnow-ping/indexnow-ping-service.js +6 -8
  315. package/core/server/services/indexnow-ping/indexnow-ping-service.ts +9 -17
  316. package/core/server/services/lib/mailgun-client.js +3 -3
  317. package/core/server/services/media-inliner/external-media-inliner.js +12 -8
  318. package/core/server/services/member-attribution/url-translator.js +5 -7
  319. package/core/server/services/member-welcome-emails/service.js +72 -12
  320. package/core/server/services/members/api.js +1 -1
  321. package/core/server/services/members/import-export/import/completion-email.js +8 -4
  322. package/core/server/services/members/import-export/import/completion-email.ts +9 -4
  323. package/core/server/services/members/import-export/import/importer.js +25 -4
  324. package/core/server/services/members/import-export/import/importer.ts +26 -6
  325. package/core/server/services/members/import-export/import/row.ts +5 -1
  326. package/core/server/services/members/import-export/index.js +1 -5
  327. package/core/server/services/members/import-export/index.ts +2 -6
  328. package/core/server/services/members/members-api/repositories/member-repository.js +51 -0
  329. package/core/server/services/members-custom-fields/queries.js +4 -13
  330. package/core/server/services/members-custom-fields/queries.ts +4 -13
  331. package/core/server/services/members-custom-fields/schema.js +18 -27
  332. package/core/server/services/members-custom-fields/schema.ts +17 -27
  333. package/core/server/services/members-custom-fields/storage.js +94 -27
  334. package/core/server/services/members-custom-fields/storage.ts +114 -39
  335. package/core/server/services/members-custom-fields/values-service.js +98 -219
  336. package/core/server/services/members-custom-fields/values-service.ts +110 -238
  337. package/core/server/services/mentions/resource-service.js +1 -1
  338. package/core/server/services/mentions/service.js +2 -2
  339. package/core/server/services/route-settings/dynamic-routing-service.js +9 -59
  340. package/core/server/services/slack-notifications/slack-notifications.js +2 -2
  341. package/core/server/services/slack-ping/index.js +1 -1
  342. package/core/server/services/slack-ping/index.ts +1 -1
  343. package/core/server/services/slack-ping/slack-ping-service.js +3 -2
  344. package/core/server/services/slack-ping/slack-ping-service.ts +4 -5
  345. package/core/server/services/stats/content-stats-service.js +3 -3
  346. package/core/server/services/stats/members-stats-service.js +0 -7
  347. package/core/server/services/stats/posts-stats-service.js +2 -2
  348. package/core/server/services/themes/storage.js +19 -2
  349. package/core/server/services/url/config.js +90 -135
  350. package/core/server/services/url/index.js +6 -49
  351. package/core/server/services/url/lazy-find-resource.js +14 -11
  352. package/core/server/services/url/lazy-find-resource.ts +14 -11
  353. package/core/server/services/url/lazy-url-service.js +174 -94
  354. package/core/server/services/url/lazy-url-service.ts +233 -97
  355. package/core/server/services/url/routable-resources.js +62 -71
  356. package/core/server/services/url/router-filter.js +12 -4
  357. package/core/server/services/url/router-filter.ts +12 -4
  358. package/core/server/services/webhooks/index.js +1 -1
  359. package/core/server/services/webhooks/serialize.js +12 -2
  360. package/core/server/web/api/endpoints/admin/routes.js +2 -2
  361. package/core/shared/config/env/config.testing-mysql.json +1 -2
  362. package/core/shared/config/env/config.testing.json +1 -2
  363. package/core/shared/custom-theme-settings-cache/custom-theme-settings-bread-service.js +4 -0
  364. package/core/shared/custom-theme-settings-cache/custom-theme-settings-service.js +39 -0
  365. package/core/shared/events/index.js +1 -2
  366. package/core/shared/labs.js +4 -4
  367. package/package.json +10 -11
  368. package/pnpm-lock.yaml +44 -47
  369. package/pnpm-workspace.yaml +19 -19
  370. package/tsconfig.json +1 -0
  371. package/components/tryghost-adapter-base-scheduling-0.2.0.tgz +0 -0
  372. package/components/tryghost-kg-converters-1.2.3.tgz +0 -0
  373. package/components/tryghost-kg-default-cards-10.3.3.tgz +0 -0
  374. package/components/tryghost-kg-default-nodes-2.1.4.tgz +0 -0
  375. package/core/built/admin/assets/Combination-BCm9FL4o.js +0 -41
  376. package/core/built/admin/assets/activitypub/layout-CTgrezN7.js +0 -1
  377. package/core/built/admin/assets/automated-emails-6t2bIRWf.js +0 -7
  378. package/core/built/admin/assets/checkbox-BIl077a0.js +0 -1
  379. package/core/built/admin/assets/chunk.524.87b52f4fada529df2c4c.js +0 -25
  380. package/core/built/admin/assets/chunk.582.340bcca873e806cbd4bb.js +0 -27
  381. package/core/built/admin/assets/code-editor-view-C0aWNwdl.js +0 -1
  382. package/core/built/admin/assets/comments-qwhIjH3c.js +0 -1
  383. package/core/built/admin/assets/disabled-sources-indicator-DYAXW-6V.js +0 -1
  384. package/core/built/admin/assets/dist-BUKmD51u.js +0 -1
  385. package/core/built/admin/assets/dist-CNlMj7ML.js +0 -1
  386. package/core/built/admin/assets/dist-DDY6GRVz.js +0 -1
  387. package/core/built/admin/assets/dist-DG78yroX.js +0 -23
  388. package/core/built/admin/assets/editor-DDYSOXGN.js +0 -8
  389. package/core/built/admin/assets/esm-CFX93H5M.js +0 -2
  390. package/core/built/admin/assets/filters-CLehPo_D.js +0 -1
  391. package/core/built/admin/assets/index-08n05-rc.js +0 -67
  392. package/core/built/admin/assets/index-DgUzgFvS.css +0 -1
  393. package/core/built/admin/assets/label-picker-DVnpu_IF.js +0 -1
  394. package/core/built/admin/assets/layout-B0YLh7R9.js +0 -1
  395. package/core/built/admin/assets/member-custom-fields-C8NpZYza.js +0 -1
  396. package/core/built/admin/assets/member-detail-CIVAdKXf.js +0 -1
  397. package/core/built/admin/assets/members-oLPiSkXD.js +0 -10
  398. package/core/built/admin/assets/modals-CB7v-k3T.js +0 -31
  399. package/core/built/admin/assets/newsletter-rbeDX9Wk.js +0 -1
  400. package/core/built/admin/assets/overview--4V-kx9c.js +0 -1
  401. package/core/built/admin/assets/overview-DSLv30oz.js +0 -1
  402. package/core/built/admin/assets/post-analytics-header-pU2m9ZuV.js +0 -1
  403. package/core/built/admin/assets/preferences-Ru4CRPHC.js +0 -5
  404. package/core/built/admin/assets/profile-BNb1oj7D.js +0 -1
  405. package/core/built/admin/assets/select-B0HteHrp.js +0 -1
  406. package/core/built/admin/assets/settings-B1C9Q3wo.js +0 -4
  407. package/core/built/admin/assets/social-urls-DA2lXrhW.js +0 -8
  408. package/core/built/admin/assets/switch-CyQcwMAv.js +0 -1
  409. package/core/built/admin/assets/tabs-D4y1gqUd.js +0 -1
  410. package/core/built/admin/assets/tags-Dmft-Y4O.js +0 -1
  411. package/core/built/admin/assets/toggle-group-R2QMFvXW.js +0 -1
  412. package/core/built/admin/assets/use-filter-params-WrdhAISv.js +0 -1
  413. package/core/built/admin/assets/web-BqW0b0_C.js +0 -1
  414. package/core/built/admin/assets/web-Cy5GKMWM.js +0 -2
  415. package/core/server/services/email-analytics/jobs/update-member-email-analytics/index.js +0 -14
  416. package/core/server/services/url/local-file-cache.js +0 -75
  417. package/core/server/services/url/queue.js +0 -260
  418. package/core/server/services/url/resource.js +0 -102
  419. package/core/server/services/url/resources.js +0 -480
  420. package/core/server/services/url/url-generator.js +0 -276
  421. package/core/server/services/url/url-service-facade.js +0 -366
  422. package/core/server/services/url/url-service-facade.ts +0 -519
  423. package/core/server/services/url/url-service.js +0 -387
  424. package/core/server/services/url/urls.js +0 -140
  425. package/core/shared/events/url-resource-updated-event.js +0 -33
  426. /package/core/built/admin/assets/{chunk.211.6f7328d63c41536451b3.js.LICENSE.txt → chunk.571.40f093ed6d883993714e.js.LICENSE.txt} +0 -0
  427. /package/core/built/admin/assets/{dist-CqO2lWS0.js → dist-CqO2lWS02.js} +0 -0
  428. /package/core/server/api/endpoints/{gift-reminders.js → gifts.js} +0 -0
@@ -0,0 +1,341 @@
1
+ import _ from 'lodash';
2
+ import debugFactory from '@tryghost/debug';
3
+ // @ts-expect-error This module lacks type definitions.
4
+ import db from '../../../data/db';
5
+ import logging from '@tryghost/logging';
6
+ import ObjectID from 'bson-objectid';
7
+
8
+ const debug = debugFactory('services:email-analytics');
9
+
10
+ const MIN_EMAIL_COUNT_FOR_OPEN_RATE = 5;
11
+
12
+ type EmailAnalyticsJobName = string;
13
+ type EmailAnalyticsEvent = 'delivered' | 'opened' | 'failed';
14
+ type CursorSeed = {
15
+ tableName: string;
16
+ eventColumns: Partial<Record<EmailAnalyticsEvent, string>>;
17
+ };
18
+
19
+ /**
20
+ * Creates a job in the jobs table if it does not already exist.
21
+ * @param jobName - The name of the job to create.
22
+ */
23
+ async function createJobIfNotExists(jobName: EmailAnalyticsJobName): Promise<void> {
24
+ await db.knex('jobs').insert({
25
+ id: new ObjectID().toHexString(),
26
+ name: jobName,
27
+ started_at: new Date(),
28
+ created_at: new Date(),
29
+ status: 'started'
30
+ }).onConflict('name').ignore();
31
+ }
32
+
33
+ export const queries = {
34
+ /**
35
+ * Retrieves the timestamp of the last seen event for the specified email analytics events.
36
+ * @param jobName - The name of the job to update.
37
+ * @param events - The email analytics events to consider.
38
+ * @param cursorSeed - Recipient table and timestamp columns to read the initial cursor from. Used when the job has no stored timestamp yet.
39
+ * @returns The timestamp of the last seen event, or null if no events are found.
40
+ */
41
+ async getLastEventTimestamp(
42
+ jobName: EmailAnalyticsJobName,
43
+ events: EmailAnalyticsEvent[],
44
+ cursorSeed: CursorSeed
45
+ ): Promise<Date | null> {
46
+ const startDate = new Date();
47
+
48
+ let timestamps: (Date | string | null)[] = [];
49
+ const lastJobRunTimestamp = await this.getLastJobRunTimestamp(jobName);
50
+
51
+ if (lastJobRunTimestamp) {
52
+ debug(`Using job data for ${jobName}`);
53
+ timestamps = [lastJobRunTimestamp];
54
+ } else {
55
+ debug(`Job data not found for ${jobName}, using ${cursorSeed.tableName} data`);
56
+ logging.info(`Job data not found for ${jobName}, using ${cursorSeed.tableName} data`);
57
+
58
+ for (const event of events) {
59
+ const columnName = cursorSeed.eventColumns[event];
60
+ if (!columnName) {
61
+ continue;
62
+ }
63
+ const row = await db.knex(cursorSeed.tableName)
64
+ .select(db.knex.raw('MAX(??) as maxTimestamp', [columnName]))
65
+ .first();
66
+ timestamps.push(row.maxTimestamp);
67
+ }
68
+
69
+ await createJobIfNotExists(jobName);
70
+ }
71
+
72
+ // Convert string dates to Date objects for SQLite compatibility
73
+ const normalizedTimestamps = timestamps.map(date => {
74
+ if (!date) {
75
+ return null;
76
+ }
77
+ return date instanceof Date ? date : new Date(date);
78
+ });
79
+
80
+ const lastSeenEventTimestamp = _.max(normalizedTimestamps) ?? null;
81
+ debug(`getLastEventTimestamp: finished in ${Date.now() - startDate.getTime()}ms`);
82
+
83
+ return lastSeenEventTimestamp;
84
+ },
85
+
86
+ /**
87
+ * Retrieves the job data for the specified job name.
88
+ * @param jobName - The name of the job to retrieve data for.
89
+ * @returns The job data, or null if no job data is found.
90
+ */
91
+ async getJobData(jobName: EmailAnalyticsJobName) {
92
+ return await db.knex('jobs').select('finished_at', 'started_at', 'metadata').where('name', jobName).first();
93
+ },
94
+
95
+ /**
96
+ * Retrieves the timestamp of the last job run for the specified job name.
97
+ * @param jobName - The name of the job to retrieve the last run timestamp for.
98
+ * @returns The timestamp of the last job run, or null if no job data is found.
99
+ */
100
+ async getLastJobRunTimestamp(jobName: EmailAnalyticsJobName): Promise<Date | null> {
101
+ const jobData = await this.getJobData(jobName);
102
+ return jobData ? jobData.finished_at || jobData.started_at : null;
103
+ },
104
+
105
+ /**
106
+ * Sets the timestamp of the last seen event for the specified email analytics events.
107
+ * @param jobName - The name of the job to update.
108
+ * @param field - The field to update.
109
+ * @param date - The timestamp of the last seen event.
110
+ * @description
111
+ * Updates the `finished_at` or `started_at` column of the specified job in the `jobs` table with the provided timestamp.
112
+ * This is used to keep track of the last time the job was run to avoid expensive queries following reboot.
113
+ */
114
+ async setJobTimestamp(jobName: EmailAnalyticsJobName, field: 'finished' | 'started', date: Date): Promise<void> {
115
+ // Convert string dates to Date objects for SQLite compatibility
116
+ try {
117
+ debug(`Setting ${field} timestamp for job ${jobName} to ${date}`);
118
+ const updateField = field === 'finished' ? 'finished_at' : 'started_at';
119
+ const status = field === 'finished' ? 'finished' : 'started';
120
+ const result = await db.knex('jobs').update({[updateField]: date, updated_at: new Date(), status: status}).where('name', jobName);
121
+ if (result === 0) {
122
+ await db.knex('jobs').insert({
123
+ id: new ObjectID().toHexString(),
124
+ name: jobName,
125
+ [updateField]: date.toISOString(), // force to iso string for sqlite
126
+ updated_at: date.toISOString(), // force to iso string for sqlite
127
+ status: status
128
+ });
129
+ }
130
+ } catch (err: unknown) {
131
+ const message = err instanceof Error ? err.message : String(err);
132
+ debug(`Error setting ${field} timestamp for job ${jobName}: ${message}`);
133
+ }
134
+ },
135
+
136
+ /**
137
+ * Retrieves and parses the metadata JSON for the specified job.
138
+ * @param jobName - The name of the job.
139
+ * @returns The parsed metadata object, or null.
140
+ */
141
+ async getJobMetadata(jobName: EmailAnalyticsJobName): Promise<object | null> {
142
+ try {
143
+ const row = await db.knex('jobs').select('metadata').where('name', jobName).first();
144
+ if (row && row.metadata) {
145
+ return JSON.parse(row.metadata);
146
+ }
147
+ } catch (err: unknown) {
148
+ const message = err instanceof Error ? err.message : String(err);
149
+ logging.error(`Error reading metadata for job ${jobName}: ${message}`);
150
+ }
151
+ return null;
152
+ },
153
+
154
+ /**
155
+ * Writes metadata JSON for the specified job.
156
+ * @param jobName - The name of the job.
157
+ * @param metadata - The metadata to store, or null to clear.
158
+ */
159
+ async setJobMetadata(jobName: EmailAnalyticsJobName, metadata: object | null): Promise<void> {
160
+ try {
161
+ const value = metadata ? JSON.stringify(metadata) : null;
162
+ await db.knex.transaction(async (trx: typeof db.knex) => {
163
+ const result = await trx('jobs').update({metadata: value, updated_at: new Date()}).where('name', jobName);
164
+ if (result === 0 && metadata) {
165
+ await trx('jobs').insert({
166
+ id: new ObjectID().toHexString(),
167
+ name: jobName,
168
+ metadata: value,
169
+ created_at: new Date(),
170
+ status: 'queued'
171
+ });
172
+ }
173
+ });
174
+ } catch (err: unknown) {
175
+ const message = err instanceof Error ? err.message : String(err);
176
+ logging.error(`Error setting metadata for job ${jobName}: ${message}`);
177
+ }
178
+ },
179
+
180
+ /**
181
+ * Sets the status of the specified email analytics job.
182
+ * @param jobName - The name of the job to update.
183
+ * @param status - The new status of the job.
184
+ * @description
185
+ * Updates the `status` column of the specified job in the `jobs` table with the provided status.
186
+ * This is used to keep track of the current state of the job.
187
+ */
188
+ async setJobStatus(jobName: EmailAnalyticsJobName, status: 'started' | 'finished' | 'failed'): Promise<void> {
189
+ debug(`Setting status for job ${jobName} to ${status}`);
190
+ try {
191
+ const result = await db.knex('jobs')
192
+ .update({
193
+ status: status,
194
+ updated_at: new Date()
195
+ })
196
+ .where('name', jobName);
197
+
198
+ if (result === 0) {
199
+ await db.knex('jobs').insert({
200
+ id: new ObjectID().toHexString(),
201
+ name: jobName,
202
+ status: status,
203
+ created_at: new Date(),
204
+ updated_at: new Date()
205
+ });
206
+ }
207
+ } catch (err: unknown) {
208
+ const message = err instanceof Error ? err.message : String(err);
209
+ debug(`Error setting status for job ${jobName}: ${message}`);
210
+ throw err;
211
+ }
212
+ },
213
+
214
+ async aggregateEmailStats(emailId: string, updateOpenedCount: boolean): Promise<void> {
215
+ const [deliveredCount] = await db.knex('email_recipients').count('id as count').whereRaw('email_id = ? AND delivered_at IS NOT NULL', [emailId]);
216
+ const [failedCount] = await db.knex('email_recipients').count('id as count').whereRaw('email_id = ? AND failed_at IS NOT NULL', [emailId]);
217
+
218
+ const updateData: Record<string, string | number> = {
219
+ delivered_count: deliveredCount.count,
220
+ failed_count: failedCount.count
221
+ };
222
+
223
+ if (updateOpenedCount) {
224
+ const [openedCount] = await db.knex('email_recipients').count('id as count').whereRaw('email_id = ? AND opened_at IS NOT NULL', [emailId]);
225
+ updateData.opened_count = openedCount.count;
226
+ }
227
+
228
+ await db.knex('emails').update(updateData).where('id', emailId);
229
+ },
230
+
231
+ async aggregateMemberStats(memberId: string): Promise<void> {
232
+ const {trackedEmailCount} = await db.knex('email_recipients')
233
+ .select(db.knex.raw('COUNT(email_recipients.id) as trackedEmailCount'))
234
+ .leftJoin('emails', 'email_recipients.email_id', 'emails.id')
235
+ .where('email_recipients.member_id', memberId)
236
+ .where('emails.track_opens', true)
237
+ .first() || {};
238
+
239
+ const [emailCount] = await db.knex('email_recipients').count('id as count').whereRaw('member_id = ?', [memberId]);
240
+ const [emailOpenedCount] = await db.knex('email_recipients').count('id as count').whereRaw('member_id = ? AND opened_at IS NOT NULL', [memberId]);
241
+
242
+ const updateQuery: Record<string, string | number> = {
243
+ email_count: emailCount.count,
244
+ email_opened_count: emailOpenedCount.count
245
+ };
246
+
247
+ if (trackedEmailCount >= MIN_EMAIL_COUNT_FOR_OPEN_RATE) {
248
+ updateQuery.email_open_rate = Math.round(emailOpenedCount.count / trackedEmailCount * 100);
249
+ }
250
+
251
+ await db.knex('members')
252
+ .update(updateQuery)
253
+ .where('id', memberId);
254
+ },
255
+
256
+ async aggregateMemberStatsBatch(memberIds: string[]): Promise<void> {
257
+ if (!memberIds || memberIds.length === 0) {
258
+ return;
259
+ }
260
+
261
+ // Batch query to get stats for all members at once
262
+ const stats = await db.knex('email_recipients')
263
+ .leftJoin('emails', 'emails.id', 'email_recipients.email_id')
264
+ .select(
265
+ 'email_recipients.member_id',
266
+ db.knex.raw('COUNT(email_recipients.id) as email_count'),
267
+ db.knex.raw('SUM(CASE WHEN email_recipients.opened_at IS NOT NULL THEN 1 ELSE 0 END) as email_opened_count'),
268
+ db.knex.raw('SUM(CASE WHEN emails.track_opens = 1 THEN 1 ELSE 0 END) as tracked_count')
269
+ )
270
+ .whereIn('email_recipients.member_id', memberIds)
271
+ .groupBy('email_recipients.member_id');
272
+
273
+ // Build update data for each member
274
+ const memberStatsMap = new Map<string, {email_count: number; email_opened_count: number; email_open_rate: number | null}>();
275
+ for (const stat of stats) {
276
+ const emailOpenRate = stat.tracked_count >= MIN_EMAIL_COUNT_FOR_OPEN_RATE
277
+ ? Math.round((stat.email_opened_count / stat.tracked_count) * 100)
278
+ : null;
279
+
280
+ memberStatsMap.set(stat.member_id, {
281
+ email_count: stat.email_count,
282
+ email_opened_count: stat.email_opened_count,
283
+ email_open_rate: emailOpenRate
284
+ });
285
+ }
286
+
287
+ // Build CASE statements for batch update
288
+ const emailCountCases: string[] = [];
289
+ const emailOpenedCountCases: string[] = [];
290
+ const emailOpenRateCases: string[] = [];
291
+ const emailCountBindings: (string | number)[] = [];
292
+ const emailOpenedCountBindings: (string | number)[] = [];
293
+ const emailOpenRateBindings: (string | number)[] = [];
294
+
295
+ for (const memberId of memberIds) {
296
+ const memberStats = memberStatsMap.get(memberId) || {
297
+ email_count: 0,
298
+ email_opened_count: 0,
299
+ email_open_rate: null
300
+ };
301
+
302
+ emailCountCases.push(`WHEN ? THEN ?`);
303
+ emailCountBindings.push(memberId, memberStats.email_count);
304
+
305
+ emailOpenedCountCases.push(`WHEN ? THEN ?`);
306
+ emailOpenedCountBindings.push(memberId, memberStats.email_opened_count);
307
+
308
+ if (memberStats.email_open_rate !== null) {
309
+ emailOpenRateCases.push(`WHEN ? THEN ?`);
310
+ emailOpenRateBindings.push(memberId, memberStats.email_open_rate);
311
+ } else {
312
+ emailOpenRateCases.push(`WHEN ? THEN NULL`);
313
+ emailOpenRateBindings.push(memberId);
314
+ }
315
+ }
316
+
317
+ // Combine bindings in the order they appear in the SQL statement:
318
+ // 1. All bindings for email_count CASE statement
319
+ // 2. All bindings for email_opened_count CASE statement
320
+ // 3. All bindings for email_open_rate CASE statement
321
+ // 4. Member IDs for the WHERE IN clause
322
+ const bindings = [
323
+ ...emailCountBindings,
324
+ ...emailOpenedCountBindings,
325
+ ...emailOpenRateBindings,
326
+ ...memberIds
327
+ ];
328
+
329
+ // Execute batched update with CASE statements
330
+ await db.knex.raw(`
331
+ UPDATE members
332
+ SET
333
+ email_count = CASE id ${emailCountCases.join(' ')} END,
334
+ email_opened_count = CASE id ${emailOpenedCountCases.join(' ')} END,
335
+ email_open_rate = CASE id ${emailOpenRateCases.join(' ')} END
336
+ WHERE id IN (${memberIds.map(() => '?').join(',')})
337
+ `, bindings);
338
+ }
339
+ };
340
+
341
+ export type Queries = typeof queries;
@@ -31,7 +31,7 @@ class EmailServiceWrapper {
31
31
 
32
32
  const {Post, Newsletter, Email, EmailBatch, EmailRecipient, Member} = require('../../models');
33
33
  const urlService = require('../url');
34
- const getRequiredUrlRelations = () => urlService.facade.getRequiredRelations();
34
+ const getRequiredUrlRelations = () => urlService.getRequiredRelations();
35
35
  const MailgunClient = require('../lib/mailgun-client');
36
36
  const configService = require('../../../shared/config');
37
37
  const settingsCache = require('../../../shared/settings-cache');
@@ -75,6 +75,7 @@ class EmailServiceWrapper {
75
75
 
76
76
  const mailgunEmailProvider = new MailgunEmailProvider({
77
77
  mailgunClient,
78
+ config: configService,
78
79
  errorHandler
79
80
  });
80
81
 
@@ -2,6 +2,11 @@ const logging = require('@tryghost/logging');
2
2
  const errors = require('@tryghost/errors');
3
3
  const debug = require('@tryghost/debug')('email-service:mailgun-provider-service');
4
4
 
5
+ /**
6
+ * @typedef {object} Config
7
+ * @prop {(key: string) => unknown} get
8
+ */
9
+
5
10
  /**
6
11
  * @typedef {object} Recipient
7
12
  * @prop {string} email
@@ -29,18 +34,22 @@ const debug = require('@tryghost/debug')('email-service:mailgun-provider-service
29
34
 
30
35
  class MailgunEmailProvider {
31
36
  #mailgunClient;
37
+ #config;
32
38
  #errorHandler;
33
39
 
34
40
  /**
35
41
  * @param {object} dependencies
36
42
  * @param {import('../lib/mailgun-client')} dependencies.mailgunClient - mailgun client to send emails
43
+ * @param {Config} dependencies.config
37
44
  * @param {Function} [dependencies.errorHandler] - custom error handler for logging exceptions
38
45
  */
39
46
  constructor({
40
47
  mailgunClient,
48
+ config,
41
49
  errorHandler
42
50
  }) {
43
51
  this.#mailgunClient = mailgunClient;
52
+ this.#config = config;
44
53
  this.#errorHandler = errorHandler;
45
54
  }
46
55
 
@@ -108,12 +117,18 @@ class MailgunEmailProvider {
108
117
  plaintext,
109
118
  from,
110
119
  replyTo,
120
+ tags: ['bulk-email'],
111
121
  domainOverride,
112
122
  id: emailId,
113
123
  track_opens: !!options.openTrackingEnabled,
114
124
  track_clicks: !!options.clickTrackingEnabled
115
125
  };
116
126
 
127
+ const mailgunTagFromConfig = this.#config.get('bulkEmail:mailgun:tag');
128
+ if (typeof mailgunTagFromConfig === 'string' && mailgunTagFromConfig.length > 0) {
129
+ messageData.tags.push(mailgunTagFromConfig);
130
+ }
131
+
117
132
  if (options.deliveryTime && options.deliveryTime instanceof Date) {
118
133
  messageData.deliveryTime = options.deliveryTime;
119
134
  }
@@ -11,7 +11,6 @@ class IndexNowPingServiceWrapper {
11
11
  // Wire up all the dependencies
12
12
  const settingsCache = require('../../../shared/settings-cache');
13
13
  const config = require('../../../shared/config');
14
- const labs = require('../../../shared/labs');
15
14
  const urlService = require('../url');
16
15
  const urlUtils = require('../../../shared/url-utils').default;
17
16
  const request = require('@tryghost/request');
@@ -20,7 +19,6 @@ class IndexNowPingServiceWrapper {
20
19
  this.service = new indexnow_ping_service_1.IndexNowPingService({
21
20
  settingsCache,
22
21
  config,
23
- labs,
24
22
  urlService,
25
23
  urlUtils,
26
24
  request,
@@ -12,7 +12,6 @@ class IndexNowPingServiceWrapper {
12
12
  // Wire up all the dependencies
13
13
  const settingsCache = require('../../../shared/settings-cache');
14
14
  const config = require('../../../shared/config');
15
- const labs = require('../../../shared/labs');
16
15
  const urlService = require('../url');
17
16
  const urlUtils = require('../../../shared/url-utils').default;
18
17
  const request = require('@tryghost/request');
@@ -22,7 +21,6 @@ class IndexNowPingServiceWrapper {
22
21
  this.service = new IndexNowPingService({
23
22
  settingsCache,
24
23
  config,
25
- labs,
26
24
  urlService,
27
25
  urlUtils,
28
26
  request,
@@ -31,17 +31,15 @@ const constants_1 = require("./constants");
31
31
  class IndexNowPingService {
32
32
  settingsCache;
33
33
  config;
34
- labs;
35
34
  urlService;
36
35
  urlUtils;
37
36
  request;
38
37
  logging;
39
38
  events;
40
39
  listener;
41
- constructor({ settingsCache, config, labs, urlService, urlUtils, request, logging, events }) {
40
+ constructor({ settingsCache, config, urlService, urlUtils, request, logging, events }) {
42
41
  this.settingsCache = settingsCache;
43
42
  this.config = config;
44
- this.labs = labs;
45
43
  this.urlService = urlService;
46
44
  this.urlUtils = urlUtils;
47
45
  this.request = request;
@@ -68,6 +66,10 @@ class IndexNowPingService {
68
66
  * Ping IndexNow with a URL.
69
67
  */
70
68
  async ping(post) {
69
+ // Skip if in development
70
+ if (this.config.get('env') === 'development') {
71
+ return;
72
+ }
71
73
  // Skip pages - only ping for posts
72
74
  if (post.type === 'page') {
73
75
  return;
@@ -80,10 +82,6 @@ class IndexNowPingService {
80
82
  if (this.config.isPrivacyDisabled('useIndexNow')) {
81
83
  return;
82
84
  }
83
- // Skip if IndexNow is not enabled in labs
84
- if (!this.labs.isSet('indexnow')) {
85
- return;
86
- }
87
85
  // Don't ping for the default posts
88
86
  if (post.slug && constants_1.defaultPostSlugs.indexOf(post.slug) > -1) {
89
87
  return;
@@ -94,7 +92,7 @@ class IndexNowPingService {
94
92
  // publishing.
95
93
  let url = null;
96
94
  try {
97
- url = this.urlService.facade.getUrlForResource({ ...post, type: 'posts' }, { absolute: true });
95
+ url = this.urlService.getUrlForResource({ ...post, type: 'posts' }, { absolute: true });
98
96
  if (!url || url.endsWith('/404/')) {
99
97
  this.logging.warn({
100
98
  event: { name: 'indexnow.unresolved_url' },
@@ -36,17 +36,12 @@ type SettingsCache = {
36
36
  };
37
37
 
38
38
  type Config = {
39
+ get(key: string): unknown;
39
40
  isPrivacyDisabled(key: string): boolean;
40
41
  };
41
42
 
42
- type Labs = {
43
- isSet(flag: string): boolean;
44
- };
45
-
46
43
  type UrlService = {
47
- facade: {
48
- getUrlForResource(resource: Record<string, unknown>, options: {absolute: boolean}): string | null;
49
- };
44
+ getUrlForResource(resource: Record<string, unknown>, options: {absolute: boolean}): string | null;
50
45
  };
51
46
 
52
47
  type UrlUtils = {
@@ -86,7 +81,6 @@ type Post = {
86
81
  export type IndexNowPingServiceDeps = {
87
82
  settingsCache: SettingsCache;
88
83
  config: Config;
89
- labs: Labs;
90
84
  urlService: UrlService;
91
85
  urlUtils: UrlUtils;
92
86
  request: RequestFn;
@@ -97,7 +91,6 @@ export type IndexNowPingServiceDeps = {
97
91
  export class IndexNowPingService {
98
92
  settingsCache: SettingsCache;
99
93
  config: Config;
100
- labs: Labs;
101
94
  urlService: UrlService;
102
95
  urlUtils: UrlUtils;
103
96
  request: RequestFn;
@@ -105,10 +98,9 @@ export class IndexNowPingService {
105
98
  events: ModelEvents;
106
99
  listener: ModelEventListener;
107
100
 
108
- constructor({settingsCache, config, labs, urlService, urlUtils, request, logging, events}: IndexNowPingServiceDeps) {
101
+ constructor({settingsCache, config, urlService, urlUtils, request, logging, events}: IndexNowPingServiceDeps) {
109
102
  this.settingsCache = settingsCache;
110
103
  this.config = config;
111
- this.labs = labs;
112
104
  this.urlService = urlService;
113
105
  this.urlUtils = urlUtils;
114
106
  this.request = request;
@@ -139,6 +131,11 @@ export class IndexNowPingService {
139
131
  * Ping IndexNow with a URL.
140
132
  */
141
133
  async ping(post: Post): Promise<void> {
134
+ // Skip if in development
135
+ if (this.config.get('env') === 'development') {
136
+ return;
137
+ }
138
+
142
139
  // Skip pages - only ping for posts
143
140
  if (post.type === 'page') {
144
141
  return;
@@ -154,11 +151,6 @@ export class IndexNowPingService {
154
151
  return;
155
152
  }
156
153
 
157
- // Skip if IndexNow is not enabled in labs
158
- if (!this.labs.isSet('indexnow')) {
159
- return;
160
- }
161
-
162
154
  // Don't ping for the default posts
163
155
  if (post.slug && defaultPostSlugs.indexOf(post.slug) > -1) {
164
156
  return;
@@ -170,7 +162,7 @@ export class IndexNowPingService {
170
162
  // publishing.
171
163
  let url: string | null = null;
172
164
  try {
173
- url = this.urlService.facade.getUrlForResource({...post, type: 'posts'}, {absolute: true});
165
+ url = this.urlService.getUrlForResource({...post, type: 'posts'}, {absolute: true});
174
166
 
175
167
  if (!url || url.endsWith('/404/')) {
176
168
  this.logging.warn({
@@ -85,9 +85,9 @@ module.exports = class MailgunClient {
85
85
  messageData['v:email-id'] = message.id;
86
86
  }
87
87
 
88
- const tags = ['bulk-email', 'ghost-email'];
89
- if (bulkEmailConfig?.mailgun?.tag) {
90
- tags.push(bulkEmailConfig.mailgun.tag);
88
+ const tags = ['ghost-email'];
89
+ if (Array.isArray(message.tags)) {
90
+ tags.push(...message.tags);
91
91
  }
92
92
  messageData['o:tag'] = tags;
93
93
 
@@ -5,7 +5,6 @@ const errors = require('@tryghost/errors');
5
5
  const logging = require('@tryghost/logging');
6
6
  const string = require('@tryghost/string');
7
7
  const path = require('path');
8
- const convert = require('heic-convert');
9
8
 
10
9
  let fileTypeFromBuffer;
11
10
 
@@ -102,20 +101,25 @@ class ExternalMediaInliner {
102
101
  }
103
102
 
104
103
  // If the file is heic or heif, attempt to convert it to jpeg
105
- try {
106
- if (extension === 'heic' || extension === 'heif') {
104
+ if (extension === 'heic' || extension === 'heif') {
105
+ // Lazy: pulls in libheif-js, a WASM codec costing ~50ms to load at boot.
106
+ // Deliberately outside the try — a missing codec is not a conversion error,
107
+ // and swallowing it would store an unconvertible .heic in its place.
108
+ const convert = require('heic-convert');
109
+
110
+ try {
107
111
  body = await convert({
108
112
  buffer: body,
109
113
  format: 'JPEG'
110
114
  });
111
115
 
112
116
  extension = 'jpg';
117
+ } catch (error) {
118
+ logging.error(`Error converting file to JPEG: ${requestURL}`);
119
+ logging.error(new errors.DataImportError({
120
+ err: error
121
+ }));
113
122
  }
114
- } catch (error) {
115
- logging.error(`Error converting file to JPEG: ${requestURL}`);
116
- logging.error(new errors.DataImportError({
117
- err: error
118
- }));
119
123
  }
120
124
 
121
125
  const removeExtRegExp = new RegExp(`.${extension}`, '');
@@ -1,9 +1,7 @@
1
1
  /**
2
2
  * @typedef {Object} UrlService
3
- * @prop {{
4
- * getUrlForResource: (resource: Object, options: Object) => string,
5
- * resolveUrl: (path: string) => Promise<Object | null>
6
- * }} facade
3
+ * @prop {(resource: Object, options: Object) => string} getUrlForResource
4
+ * @prop {(path: string) => Promise<Object | null>} resolveUrl
7
5
  */
8
6
 
9
7
  const {toPlain} = require('../../lib/common/to-plain');
@@ -106,7 +104,7 @@ class UrlTranslator {
106
104
  // surface as an error.
107
105
  let resource;
108
106
  try {
109
- resource = await this.urlService.facade.resolveUrl(path);
107
+ resource = await this.urlService.resolveUrl(path);
110
108
  } catch (err) {
111
109
  return;
112
110
  }
@@ -155,10 +153,10 @@ class UrlTranslator {
155
153
  }
156
154
  // Lazy URL service evaluates permalink templates against resource fields
157
155
  // (slug, published_at, primary_tag, ...). Caller already loaded the model,
158
- // so spread its plain data so those fields reach the facade.
156
+ // so spread its plain data so those fields reach the URL service.
159
157
  const data = toPlain(model);
160
158
  const resource = {...data, id, type: TYPE_TO_RESOURCE[type]};
161
- return this.urlService.facade.getUrlForResource(resource, {absolute});
159
+ return this.urlService.getUrlForResource(resource, {absolute});
162
160
  }
163
161
 
164
162
  async getResourceById(id, type) {