ghost 6.56.0 → 6.57.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 (385) 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-custom-field-types-0.0.0.tgz +0 -0
  5. package/components/tryghost-kg-html-to-lexical-1.3.3.tgz +0 -0
  6. package/components/tryghost-kg-lexical-html-renderer-1.4.3.tgz +0 -0
  7. package/components/tryghost-parse-email-address-0.0.0.tgz +0 -0
  8. package/core/app.js +1 -1
  9. package/core/boot.js +21 -25
  10. package/core/bridge.js +10 -12
  11. package/core/built/admin/assets/Combination-BDvCdg1t.js +41 -0
  12. package/core/built/admin/assets/about-C7O-AgRZ.js +1 -0
  13. package/core/built/admin/assets/{account-migration-D3O_yVck.js → account-migration-Cv_ol1ko.js} +1 -1
  14. package/core/built/admin/assets/action-list-lGzZLHc8.js +1 -0
  15. package/core/built/admin/assets/activitypub/{account-migration-D8FbP-Z-.js → account-migration-DHFbePl6.js} +7 -7
  16. package/core/built/admin/assets/activitypub/activitypub.js +3 -3
  17. package/core/built/admin/assets/activitypub/{avatar-11EA1VSs.js → avatar-CT4tS2_S.js} +2 -2
  18. package/core/built/admin/assets/activitypub/{avatar-flipboard-jfbLG-xe.js → avatar-flipboard-ByYWKipo.js} +2 -2
  19. package/core/built/admin/assets/activitypub/{bluesky-sharing-f1jTQe2P.js → bluesky-sharing-srVnSHDz.js} +8 -8
  20. package/core/built/admin/assets/activitypub/{content-formatters-nmTOPY9M.js → content-formatters-C2sos5Ht.js} +3 -3
  21. package/core/built/admin/assets/activitypub/{copy-BCwo9ep3.js → copy-B1UBTOoZ.js} +2 -2
  22. package/core/built/admin/assets/activitypub/{dist-BEfX-0aW.js → dist-MPsb2WZ1.js} +4 -4
  23. package/core/built/admin/assets/activitypub/{domain-AaDlZlrA.js → domain-DzcM9Dzo.js} +9 -9
  24. package/core/built/admin/assets/activitypub/{edit-profile-DPi8Tl5M.js → edit-profile-CsQ_4xAk.js} +5 -5
  25. package/core/built/admin/assets/activitypub/{explore-CxTzjILQ.js → explore-CJe0DZBK.js} +8 -8
  26. package/core/built/admin/assets/activitypub/{feed-DPv5x2MX.js → feed-CKV3qWON.js} +9 -9
  27. package/core/built/admin/assets/activitypub/{field-DbgA8lat.js → field-DckHfDJf.js} +3 -3
  28. package/core/built/admin/assets/activitypub/{hash-BWuZoYkA.js → hash-Dlp8RM55.js} +2 -2
  29. package/core/built/admin/assets/activitypub/{inbox-35VDQ2K2.js → inbox-CsPy4R0M.js} +2 -2
  30. package/core/built/admin/assets/activitypub/{inbox-DSrF_i1O.js → inbox-DAsjzUUe.js} +10 -10
  31. package/core/built/admin/assets/activitypub/{label-CPjFBbo7.js → label-DWyo9E5T.js} +3 -3
  32. package/core/built/admin/assets/activitypub/layout-DM_3pqsU.js +1 -0
  33. package/core/built/admin/assets/activitypub/{moderation-BkDC1XVZ.js → moderation-B0Inc03M.js} +7 -7
  34. package/core/built/admin/assets/activitypub/{note-xEyH4xT-.js → note-CSE54lER.js} +7 -7
  35. package/core/built/admin/assets/activitypub/{notifications-DClaL3ui.js → notifications-RSwbKqdh.js} +9 -9
  36. package/core/built/admin/assets/activitypub/{preferences-DpnOa-4Z.js → preferences-CE231n2_.js} +11 -11
  37. package/core/built/admin/assets/activitypub/{profile-Dvi9mz-6.js → profile-aaQ1j1Eo.js} +13 -13
  38. package/core/built/admin/assets/activitypub/{reply-Cs9ySd6Z.js → reply-Cjb0Vs3m.js} +2 -2
  39. package/core/built/admin/assets/activitypub/{routes-DFBeTnfn.js → routes-Djb0mc9k.js} +26 -26
  40. package/core/built/admin/assets/activitypub/{separator-C5u_VjzY.js → separator-D8pROI5n.js} +3 -3
  41. package/core/built/admin/assets/activitypub/{settings-CV2fFu6D.js → settings-DMc32IQl.js} +5 -5
  42. package/core/built/admin/assets/activitypub/{site-_TgDd_EM.js → site-DVIcsohX.js} +4 -4
  43. package/core/built/admin/assets/activitypub/{step-1-BfnNL2oF.js → step-1-9Yey9usF.js} +5 -5
  44. package/core/built/admin/assets/activitypub/{step-2-DLndZkO-.js → step-2-DdYmKRXV.js} +6 -6
  45. package/core/built/admin/assets/activitypub/{step-3-A-PZTtRE.js → step-3-hdSkcjZU.js} +10 -10
  46. package/core/built/admin/assets/activitypub/{tabs-BUbz7tea.js → tabs-CUqseYzQ.js} +6 -6
  47. package/core/built/admin/assets/activitypub/{topic-filter-BdX_xYYT.js → topic-filter-Coni6OXM.js} +3 -3
  48. package/core/built/admin/assets/activitypub/{use-activity-pub-queries-EwuRemHh.js → use-activity-pub-queries-MeHTPMQv.js} +2 -2
  49. package/core/built/admin/assets/activitypub/{use-navigate-with-base-path-cy4ACBc4.js → use-navigate-with-base-path-B046AM38.js} +24 -7
  50. package/core/built/admin/assets/activitypub/{use-reply-chain-data-CyFcGfan.js → use-reply-chain-data-1AO-Xob6.js} +5 -5
  51. package/core/built/admin/assets/activitypub/{x-7zk8mV2p.js → x-BRfRYd16.js} +2 -2
  52. package/core/built/admin/assets/add-integration-modal-m9Dv6f77.js +1 -0
  53. package/core/built/admin/assets/add-newsletter-modal-CPSVZL2H.js +1 -0
  54. package/core/built/admin/assets/add-recommendation-modal-Ci9Z5yat.js +1 -0
  55. package/core/built/admin/assets/analytics-DqSCVV8U.js +1 -0
  56. package/core/built/admin/assets/announcement-bar-modal-DU3avV7d.js +1 -0
  57. package/core/built/admin/assets/api-keys-BK0LylyK.js +1 -0
  58. package/core/built/admin/assets/api-keys-_iCT4dxr.js +1 -0
  59. package/core/built/admin/assets/{audience-CkBb2LlO.js → audience-DTJtXXWY.js} +1 -1
  60. package/core/built/admin/assets/automated-emails-PayPCT8L.js +1 -0
  61. package/core/built/admin/assets/{automations-DhKeS_U5.js → automations-162q5HjL.js} +1 -1
  62. package/core/built/admin/assets/{automations-cMllU545.js → automations-dtQ_MRZ7.js} +1 -1
  63. package/core/built/admin/assets/{bluesky-sharing-DPGpyD-G.js → bluesky-sharing-B8Es3AuV.js} +1 -1
  64. package/core/built/admin/assets/brand-icon-09YqFZjV.js +1 -0
  65. package/core/built/admin/assets/{breadcrumb-DIfcF5ow.js → breadcrumb-tg5zL--z.js} +1 -1
  66. package/core/built/admin/assets/{chart-helpers-DV0FkdcD.js → chart-helpers-CZElTGYL.js} +1 -1
  67. package/core/built/admin/assets/checkbox-Cyo-x7mC.js +1 -0
  68. package/core/built/admin/assets/{chunk.524.87b52f4fada529df2c4c.js → chunk.524.cd5498dd4e195f679266.js} +4 -4
  69. package/core/built/admin/assets/{chunk.582.340bcca873e806cbd4bb.js → chunk.582.6f92ec1f03734bf2ea3f.js} +6 -6
  70. package/core/built/admin/assets/{chunk.332.05991a101d67f732dec3.js → chunk.873.b8fc05a8ab877fb819ed.js} +28 -27
  71. package/core/built/admin/assets/code-editor-view-Dq1rQ4tS.js +1 -0
  72. package/core/built/admin/assets/color-picker-field-BonBgxhU.js +1 -0
  73. package/core/built/admin/assets/color-utils-BsrwXuDz.js +1 -0
  74. package/core/built/admin/assets/combobox-uX5r_paI.js +1 -0
  75. package/core/built/admin/assets/command-DxwBO2pF.js +1 -0
  76. package/core/built/admin/assets/comments-Dn-RO-nU.js +1 -0
  77. package/core/built/admin/assets/components-CwL6a6As-B0E0-NBJ.js +1 -0
  78. package/core/built/admin/assets/confirmation-modal-B6D6mk6R.js +1 -0
  79. package/core/built/admin/assets/content-api-modal-BxQ-7T6u.js +1 -0
  80. package/core/built/admin/assets/copy-field-BAxL4Zqn.js +1 -0
  81. package/core/built/admin/assets/currency-Dy4Tc2R2.js +1 -0
  82. package/core/built/admin/assets/custom-integration-modal---d_l7lc.js +1 -0
  83. package/core/built/admin/assets/{date-range-select-dXc-GsVe.js → date-range-select-PB2n1HZf.js} +1 -1
  84. package/core/built/admin/assets/debounce-Dg2fW5Rg.js +3 -0
  85. package/core/built/admin/assets/design-and-theme-modal-DYRT207a.js +23 -0
  86. package/core/built/admin/assets/dirty-confirm-dialog-BnDJ39ao.js +1 -0
  87. package/core/built/admin/assets/disabled-sources-indicator-iMHrkyV3.js +1 -0
  88. package/core/built/admin/assets/dist-B2PvRwLn.js +1 -0
  89. package/core/built/admin/assets/{dist-pHiOxyxa.js → dist-D5vhMdXf.js} +1 -1
  90. package/core/built/admin/assets/dist-DlCyLrOC.js +1 -0
  91. package/core/built/admin/assets/dist-WeOIvSYM.js +23 -0
  92. package/core/built/admin/assets/{dist-DyViJwIy.js → dist-cFzKLG7y.js} +3 -3
  93. package/core/built/admin/assets/dist-j1xxxnY8.js +1 -0
  94. package/core/built/admin/assets/dist-myQaY0ka.js +1 -0
  95. package/core/built/admin/assets/{domain-B5ogXUBa.js → domain-BR8FIdjD.js} +1 -1
  96. package/core/built/admin/assets/{dropzone-BnFG7dDP.js → dropzone-hNxmUvwD.js} +1 -1
  97. package/core/built/admin/assets/{edit-profile-B07jCZy9.js → edit-profile-CENrkY0l.js} +1 -1
  98. package/core/built/admin/assets/editor-B18D0lm8.js +8 -0
  99. package/core/built/admin/assets/embed-signup-form-modal-BsCYUMvQ.js +9 -0
  100. package/core/built/admin/assets/{explore-Ca1y8HW_.js → explore-BmrnljWw.js} +1 -1
  101. package/core/built/admin/assets/{feed-x4iFMBP4.js → feed-Dy4li6i8.js} +1 -1
  102. package/core/built/admin/assets/{field-ChCAabZg.js → field-Dh_lwk13.js} +1 -1
  103. package/core/built/admin/assets/{filter-sources-LdtH1e-T.js → filter-sources-D2zDuJH5.js} +1 -1
  104. package/core/built/admin/assets/filters-3JBQcNtJ.js +1 -0
  105. package/core/built/admin/assets/{filters-CjVKRP8U.js → filters-CFt_BDe4.js} +1 -1
  106. package/core/built/admin/assets/first-promoter-modal-D6Rjxugw.js +1 -0
  107. package/core/built/admin/assets/format-url-C-t8G6Qd.js +1 -0
  108. package/core/built/admin/assets/{ghost-7f01131115186d1af84ead76e79646dd.js → ghost-d683fbeb945b13155ea669caa5d8db41.js} +300 -298
  109. package/core/built/admin/assets/{gift-link-modal-Buqh2vXh.js → gift-link-modal-zMtlJhLQ.js} +1 -1
  110. package/core/built/admin/assets/{gift-links-CsUhefWG.js → gift-links-CVuqLhsZ.js} +1 -1
  111. package/core/built/admin/assets/global-data-provider-B949LDXJ.js +1 -0
  112. package/core/built/admin/assets/grid-CUciBfjf.js +1 -0
  113. package/core/built/admin/assets/{growth-Dy2UOSay.js → growth-C38Xtp8D.js} +2 -2
  114. package/core/built/admin/assets/{growth-DajOBdN0.js → growth-CFws67_i.js} +1 -1
  115. package/core/built/admin/assets/history-modal-ILAOkbxH.js +1 -0
  116. package/core/built/admin/assets/{hooks-CixO01lL.js → hooks-D6EE7BIt.js} +2 -2
  117. package/core/built/admin/assets/html-field-9a9Id_2q.js +1 -0
  118. package/core/built/admin/assets/iframe-buffering-e8vH4-Qn.js +1 -0
  119. package/core/built/admin/assets/images-CrQKVc9Q.js +1 -0
  120. package/core/built/admin/assets/{inbox-BUOLDoji.js → inbox-BrSRzyvz.js} +13 -13
  121. package/core/built/admin/assets/index-BHy2DZX-.js +67 -0
  122. package/core/built/admin/assets/index-Ds2iUoF6.css +1 -0
  123. package/core/built/admin/assets/index.es-B9tvf-t4.js +1 -0
  124. package/core/built/admin/assets/{input-group-uSK3XT7b.js → input-group-6vEPvX9S.js} +1 -1
  125. package/core/built/admin/assets/integration-header-B4DyXV06.js +1 -0
  126. package/core/built/admin/assets/integrations-BT6_iJvu.js +1 -0
  127. package/core/built/admin/assets/invite-user-modal-5xDCEw90.js +1 -0
  128. package/core/built/admin/assets/isEmail-BOfC647y.js +1 -0
  129. package/core/built/admin/assets/koenig-editor-base-BWLEB9Vf.js +1 -0
  130. package/core/built/admin/assets/{koenig-lexical-BFc4yFed.js → koenig-lexical-Cj62r_y9.js} +1 -1
  131. package/core/built/admin/assets/{kpi-card-BN-E6WYX.js → kpi-card-DOUnkZcN.js} +1 -1
  132. package/core/built/admin/assets/{kpi-card-CCt-qeWD.js → kpi-card-IHDo8Q56.js} +1 -1
  133. package/core/built/admin/assets/{kpi-helpers-DlxTbTs4.js → kpi-helpers-CjUdECAK.js} +1 -1
  134. package/core/built/admin/assets/{kpi-tabs-BlcUttpn.js → kpi-tabs-X3DkOYbI.js} +1 -1
  135. package/core/built/admin/assets/{label-AVrxwmsz.js → label-Dq6QaOuS.js} +1 -1
  136. package/core/built/admin/assets/label-picker-8YlhS2U3.js +1 -0
  137. package/core/built/admin/assets/{labels-DAIqKed7.js → labels-DAPjKPy2.js} +1 -1
  138. package/core/built/admin/assets/layout-TMj4P-vv.js +1 -0
  139. package/core/built/admin/assets/limit-modal-BRPsit_B.js +1 -0
  140. package/core/built/admin/assets/{link-helpers-D9LsDOkG.js → link-helpers-Bzkk68GD.js} +1 -1
  141. package/core/built/admin/assets/member-custom-fields-CmvsuuV_.js +1 -0
  142. package/core/built/admin/assets/member-detail-DCvRrkmJ.js +1 -0
  143. package/core/built/admin/assets/members-DxbSKngD.js +11 -0
  144. package/core/built/admin/assets/modal-page-BMTzFHVO.js +1 -0
  145. package/core/built/admin/assets/{moderation-CQ3urTFE.js → moderation-Da5lhhWh.js} +1 -1
  146. package/core/built/admin/assets/multi-select-combobox-CYdkH7KQ.js +1 -0
  147. package/core/built/admin/assets/navigation-modal-BIoTwtMy.js +1 -0
  148. package/core/built/admin/assets/newsletter-BptlVoJm.js +1 -0
  149. package/core/built/admin/assets/newsletter-detail-modal-BIvxqE0N.js +1 -0
  150. package/core/built/admin/assets/newsletter-emails-Bf8IZLsP.js +1 -0
  151. package/core/built/admin/assets/{newsletters-vt3Ni1Ov.js → newsletters-BgrIZk0V.js} +1 -1
  152. package/core/built/admin/assets/{newsletters-BXJbJgA-.js → newsletters-Du0lzskS.js} +1 -1
  153. package/core/built/admin/assets/{note-Dk877BTv.js → note-DYcLOmDb.js} +1 -1
  154. package/core/built/admin/assets/{notifications-CPlLjPxO.js → notifications-B2SPFIV9.js} +1 -1
  155. package/core/built/admin/assets/{offers-DEniVRD5.js → offers-Um6ab71O.js} +1 -1
  156. package/core/built/admin/assets/offers-container-modal-BZnZITMw.js +1 -0
  157. package/core/built/admin/assets/{onboarding-route-DdStOtjy.js → onboarding-route-DgdfxkQh.js} +1 -1
  158. package/core/built/admin/assets/overview-BYQJXSog.js +1 -0
  159. package/core/built/admin/assets/overview-Dt2_mR6U.js +1 -0
  160. package/core/built/admin/assets/{page-header-pp4dWmOS.js → page-header-CpmxJj5B.js} +1 -1
  161. package/core/built/admin/assets/{pages-B4goRPUn.js → pages-BVMnpI2O.js} +1 -1
  162. package/core/built/admin/assets/pintura-modal-B4MuBuuO.js +1 -0
  163. package/core/built/admin/assets/portal-frame-BF0NBIji.js +1 -0
  164. package/core/built/admin/assets/portal-modal-DMVbm0oy.js +1 -0
  165. package/core/built/admin/assets/{post-analytics-BMJrI81R.js → post-analytics-CXWBOGyP.js} +1 -1
  166. package/core/built/admin/assets/post-analytics-header-ChzjXVPt.js +1 -0
  167. package/core/built/admin/assets/{post-analytics-provider-Btml5vJI.js → post-analytics-provider-Cgw4U_fw.js} +1 -1
  168. package/core/built/admin/assets/{post-share-modal-AcUZkbfL.js → post-share-modal-B9Vmk_XM.js} +1 -1
  169. package/core/built/admin/assets/{posts-DhXE7b6r.js → posts-CxG7v0-M.js} +1 -1
  170. package/core/built/admin/assets/preferences-BfzJmnsd.js +5 -0
  171. package/core/built/admin/assets/preview-chrome-BOVO6_PE.js +1 -0
  172. package/core/built/admin/assets/preview-modal-Dw_HtD6S.js +1 -0
  173. package/core/built/admin/assets/profile-BT5vtv9b.js +1 -0
  174. package/core/built/admin/assets/radio-group-I7ou7PoJ.js +1 -0
  175. package/core/built/admin/assets/{referrers-pmJT4k3-.js → referrers-B9QDAfYm.js} +1 -1
  176. package/core/built/admin/assets/{router-provider-BmEZf6c6.js → router-provider-DSQfprxH.js} +1 -1
  177. package/core/built/admin/assets/select-D9oU1IoM.js +1 -0
  178. package/core/built/admin/assets/settings-CfZFbyu_.js +4 -0
  179. package/core/built/admin/assets/{settings-BGgk7oCI.js → settings-CpvHWix7.js} +1 -1
  180. package/core/built/admin/assets/settings-breadcrumbs-K0USP2nw.js +1 -0
  181. package/core/built/admin/assets/{share-modal-BeuydaBf.js → share-modal-ClIyPzc5.js} +1 -1
  182. package/core/built/admin/assets/slack-modal-1XXqOfiw.js +1 -0
  183. package/core/built/admin/assets/social-urls-CHQ9cTAK.js +1 -0
  184. package/core/built/admin/assets/{sort-button-Df4t2AL8.js → sort-button-CdBsm40v.js} +1 -1
  185. package/core/built/admin/assets/{source-icon-ajoaKb41.js → source-icon-CEYmVLXR.js} +1 -1
  186. package/core/built/admin/assets/{stats-C_lfxZAw.js → stats-D8nZCTy5.js} +1 -1
  187. package/core/built/admin/assets/{stats-view-Cxot1iD8.js → stats-view-CugFVf0g.js} +1 -1
  188. package/core/built/admin/assets/{step-1-ogAA4lDr.js → step-1-DOYjrORn.js} +1 -1
  189. package/core/built/admin/assets/{step-2-C5Xini-7.js → step-2-vA6RsarH.js} +1 -1
  190. package/core/built/admin/assets/{step-3-CZv1FV81.js → step-3-BtIBxMId.js} +2 -2
  191. package/core/built/admin/assets/stripe-button-UFP4QRXd.js +1 -0
  192. package/core/built/admin/assets/stripe-connect-modal-C_L3uM6n.js +1 -0
  193. package/core/built/admin/assets/switch-CE2NuMn-.js +1 -0
  194. package/core/built/admin/assets/{table-DFj1wiYv.js → table-TMCgCQK7.js} +1 -1
  195. package/core/built/admin/assets/tabs-Dd6oOKbv.js +1 -0
  196. package/core/built/admin/assets/tag-detail-D3l-UJPR.js +1 -0
  197. package/core/built/admin/assets/tags-BwCgmr5v.js +1 -0
  198. package/core/built/admin/assets/tags-WdKQGWR8.js +1 -0
  199. package/core/built/admin/assets/testimonials-modal-CqWp-xXJ.js +1 -0
  200. package/core/built/admin/assets/tier-detail-modal-BkcR2u0A.js +1 -0
  201. package/core/built/admin/assets/{tiers-DYsa4DCo.js → tiers-Da6kxHQP.js} +1 -1
  202. package/core/built/admin/assets/toggle-group-Guorw93z.js +1 -0
  203. package/core/built/admin/assets/{topic-filter-DGDxDalo.js → topic-filter-CbgoWIHn.js} +1 -1
  204. package/core/built/admin/assets/transistor-modal-DuNnnpal.js +1 -0
  205. package/core/built/admin/assets/unsplash-modal-y2bVwTHH.js +1 -0
  206. package/core/built/admin/assets/{use-analytics-data-C76v4LIp.js → use-analytics-data-B-b4wNSC.js} +1 -1
  207. package/core/built/admin/assets/use-check-theme-limit-error-m09J38Mq.js +2 -0
  208. package/core/built/admin/assets/use-feature-flag-ud0Qz8vt.js +1 -0
  209. package/core/built/admin/assets/use-filter-params-BnbBDc7T.js +1 -0
  210. package/core/built/admin/assets/use-filterable-api-iMpDZBxD.js +1 -0
  211. package/core/built/admin/assets/use-form-vRjI0Kwk.js +1 -0
  212. package/core/built/admin/assets/{use-growth-stats-aNXJOmQr.js → use-growth-stats-DPU5rt3W.js} +1 -1
  213. package/core/built/admin/assets/use-hash-link-navigation-guard-B9j6HIrV.js +1 -0
  214. package/core/built/admin/assets/use-limiter-BkN3E4gw.js +2 -0
  215. package/core/built/admin/assets/use-pintura-config-C_z8HTG2.js +1 -0
  216. package/core/built/admin/assets/use-pintura-editor-DNgbkhGp.js +1 -0
  217. package/core/built/admin/assets/{use-post-referrers-CG0SZioj.js → use-post-referrers-C2mXyrp-.js} +1 -1
  218. package/core/built/admin/assets/use-query-params-DHhhu8wi.js +1 -0
  219. package/core/built/admin/assets/{use-reply-chain-data-RLdPKRmS.js → use-reply-chain-data-DaK8q2qQ.js} +1 -1
  220. package/core/built/admin/assets/use-setting-group-YlYcoNyP.js +1 -0
  221. package/core/built/admin/assets/use-settings-navigation-Bf1g1rEh.js +1 -0
  222. package/core/built/admin/assets/{use-simple-pagination-DgcF69KI.js → use-simple-pagination-D9ckKo5-.js} +1 -1
  223. package/core/built/admin/assets/{use-tinybird-query-C1R_Kbmy.js → use-tinybird-query-GSa32hVw.js} +1 -1
  224. package/core/built/admin/assets/{use-tinybird-token-C9fPjVM_.js → use-tinybird-token-CKLOZFsw.js} +1 -1
  225. package/core/built/admin/assets/use-url-input-z-p0Kewm.js +5 -0
  226. package/core/built/admin/assets/user-detail-modal-BMxOA4Sv.js +1 -0
  227. package/core/built/admin/assets/{users-Dkv8NQZe.js → users-B6hIds6n.js} +1 -1
  228. package/core/built/admin/assets/validator-Cr0sMKuW.js +7 -0
  229. package/core/built/admin/assets/{virtual-list-BmTUzeoF.js → virtual-list-RHeEL05L.js} +1 -1
  230. package/core/built/admin/assets/web-Bw2QMO5u.js +2 -0
  231. package/core/built/admin/assets/web-xJtcwMy4.js +1 -0
  232. package/core/built/admin/assets/zapier-modal-BP2f4-vY.js +1 -0
  233. package/core/built/admin/index.html +15 -13
  234. package/core/frontend/helpers/authors.js +1 -1
  235. package/core/frontend/helpers/link.js +3 -8
  236. package/core/frontend/helpers/tags.js +1 -1
  237. package/core/frontend/meta/author-url.js +2 -2
  238. package/core/frontend/meta/url.js +4 -4
  239. package/core/frontend/services/proxy.js +1 -1
  240. package/core/frontend/services/routing/events.js +20 -6
  241. package/core/frontend/services/routing/permalink-adapter.js +0 -13
  242. package/core/frontend/services/routing/permalink-adapter.ts +0 -13
  243. package/core/frontend/services/routing/router-manager.js +15 -9
  244. package/core/frontend/services/sitemap/base-site-map-generator.js +1 -28
  245. package/core/frontend/services/sitemap/site-map-manager.js +22 -57
  246. package/core/server/adapters/route-settings/utils.js +10 -2
  247. package/core/server/adapters/route-settings/utils.ts +12 -2
  248. package/core/server/api/endpoints/email-post.js +1 -1
  249. package/core/server/api/endpoints/previews.js +1 -1
  250. package/core/server/api/endpoints/search-index-public.js +3 -3
  251. package/core/server/api/endpoints/search-index.js +4 -4
  252. package/core/server/api/endpoints/utils/serializers/input/authors.js +2 -2
  253. package/core/server/api/endpoints/utils/serializers/input/comments.js +1 -1
  254. package/core/server/api/endpoints/utils/serializers/input/pages.js +3 -3
  255. package/core/server/api/endpoints/utils/serializers/input/posts.js +3 -3
  256. package/core/server/api/endpoints/utils/serializers/input/tags.js +1 -1
  257. package/core/server/api/endpoints/utils/serializers/input/users.js +2 -2
  258. package/core/server/api/endpoints/utils/serializers/input/utils/url.js +31 -13
  259. package/core/server/api/endpoints/utils/serializers/output/mappers/posts.js +2 -2
  260. package/core/server/api/endpoints/utils/serializers/output/mappers/tags.js +2 -2
  261. package/core/server/api/endpoints/utils/serializers/output/mappers/users.js +8 -0
  262. package/core/server/api/endpoints/utils/serializers/output/utils/url.js +18 -11
  263. package/core/server/data/migrations/versions/6.57/2026-08-04-18-00-00-store-custom-field-values-as-leaf-rows.js +74 -0
  264. package/core/server/data/migrations/versions/6.57/2026-08-04-18-54-14-reset-automation-email-analytics.js +25 -0
  265. package/core/server/data/schema/commands.js +21 -5
  266. package/core/server/data/schema/schema.js +24 -5
  267. package/core/server/lib/common/to-plain.ts +1 -1
  268. package/core/server/models/post.js +2 -2
  269. package/core/server/services/adapter-manager/adapter-manager.js +1 -45
  270. package/core/server/services/adapter-manager/adapter-manager.ts +2 -48
  271. package/core/server/services/adapter-manager/adapter-paths.js +24 -0
  272. package/core/server/services/adapter-manager/adapter-paths.ts +21 -0
  273. package/core/server/services/adapter-manager/base-classes.js +22 -0
  274. package/core/server/services/adapter-manager/base-classes.ts +22 -0
  275. package/core/server/services/adapter-manager/index.js +4 -27
  276. package/core/server/services/adapter-manager/index.ts +4 -31
  277. package/core/server/services/adapter-manager/utils.js +50 -0
  278. package/core/server/services/adapter-manager/utils.ts +57 -0
  279. package/core/server/services/audience-feedback/audience-feedback-controller.js +1 -1
  280. package/core/server/services/audience-feedback/audience-feedback-service.js +2 -2
  281. package/core/server/services/automations/service.js +2 -2
  282. package/core/server/services/automations/service.ts +2 -2
  283. package/core/server/services/comments/comments-service-emails.js +1 -1
  284. package/core/server/services/comments/comments-service.js +1 -1
  285. package/core/server/services/email-analytics/email-analytics-service-wrapper.js +11 -2
  286. package/core/server/services/email-analytics/email-analytics-service.js +1 -1
  287. package/core/server/services/email-service/email-service-wrapper.js +2 -1
  288. package/core/server/services/email-service/mailgun-email-provider.js +15 -0
  289. package/core/server/services/indexnow-ping/indexnow-ping-service.js +1 -1
  290. package/core/server/services/indexnow-ping/indexnow-ping-service.ts +2 -4
  291. package/core/server/services/lib/mailgun-client.js +3 -3
  292. package/core/server/services/media-inliner/external-media-inliner.js +12 -8
  293. package/core/server/services/member-attribution/url-translator.js +5 -7
  294. package/core/server/services/member-welcome-emails/service.js +72 -12
  295. package/core/server/services/members/api.js +1 -1
  296. package/core/server/services/members/import-export/import/completion-email.js +8 -4
  297. package/core/server/services/members/import-export/import/completion-email.ts +9 -4
  298. package/core/server/services/members/import-export/import/importer.js +25 -4
  299. package/core/server/services/members/import-export/import/importer.ts +26 -6
  300. package/core/server/services/members/import-export/import/row.ts +5 -1
  301. package/core/server/services/members/import-export/index.js +1 -5
  302. package/core/server/services/members/import-export/index.ts +2 -6
  303. package/core/server/services/members-custom-fields/queries.js +4 -13
  304. package/core/server/services/members-custom-fields/queries.ts +4 -13
  305. package/core/server/services/members-custom-fields/schema.js +18 -27
  306. package/core/server/services/members-custom-fields/schema.ts +17 -27
  307. package/core/server/services/members-custom-fields/storage.js +94 -27
  308. package/core/server/services/members-custom-fields/storage.ts +114 -39
  309. package/core/server/services/members-custom-fields/values-service.js +98 -219
  310. package/core/server/services/members-custom-fields/values-service.ts +110 -238
  311. package/core/server/services/mentions/resource-service.js +1 -1
  312. package/core/server/services/mentions/service.js +2 -2
  313. package/core/server/services/route-settings/dynamic-routing-service.js +9 -59
  314. package/core/server/services/slack-ping/slack-ping-service.js +2 -2
  315. package/core/server/services/slack-ping/slack-ping-service.ts +3 -5
  316. package/core/server/services/stats/content-stats-service.js +3 -3
  317. package/core/server/services/stats/posts-stats-service.js +2 -2
  318. package/core/server/services/url/config.js +90 -135
  319. package/core/server/services/url/index.js +6 -49
  320. package/core/server/services/url/lazy-find-resource.js +14 -11
  321. package/core/server/services/url/lazy-find-resource.ts +14 -11
  322. package/core/server/services/url/lazy-url-service.js +174 -94
  323. package/core/server/services/url/lazy-url-service.ts +233 -97
  324. package/core/server/services/url/routable-resources.js +62 -71
  325. package/core/server/services/url/router-filter.js +12 -4
  326. package/core/server/services/url/router-filter.ts +12 -4
  327. package/core/server/services/webhooks/index.js +1 -1
  328. package/core/server/services/webhooks/serialize.js +2 -1
  329. package/core/shared/config/env/config.testing-mysql.json +1 -2
  330. package/core/shared/config/env/config.testing.json +1 -2
  331. package/core/shared/events/index.js +1 -2
  332. package/core/shared/labs.js +3 -3
  333. package/package.json +5 -6
  334. package/pnpm-lock.yaml +6 -9
  335. package/pnpm-workspace.yaml +2 -2
  336. package/tsconfig.json +1 -0
  337. package/core/built/admin/assets/Combination-BCm9FL4o.js +0 -41
  338. package/core/built/admin/assets/activitypub/layout-CTgrezN7.js +0 -1
  339. package/core/built/admin/assets/automated-emails-6t2bIRWf.js +0 -7
  340. package/core/built/admin/assets/checkbox-BIl077a0.js +0 -1
  341. package/core/built/admin/assets/code-editor-view-C0aWNwdl.js +0 -1
  342. package/core/built/admin/assets/comments-qwhIjH3c.js +0 -1
  343. package/core/built/admin/assets/disabled-sources-indicator-DYAXW-6V.js +0 -1
  344. package/core/built/admin/assets/dist-BUKmD51u.js +0 -1
  345. package/core/built/admin/assets/dist-CNlMj7ML.js +0 -1
  346. package/core/built/admin/assets/dist-DDY6GRVz.js +0 -1
  347. package/core/built/admin/assets/dist-DG78yroX.js +0 -23
  348. package/core/built/admin/assets/editor-DDYSOXGN.js +0 -8
  349. package/core/built/admin/assets/esm-CFX93H5M.js +0 -2
  350. package/core/built/admin/assets/filters-CLehPo_D.js +0 -1
  351. package/core/built/admin/assets/index-08n05-rc.js +0 -67
  352. package/core/built/admin/assets/index-DgUzgFvS.css +0 -1
  353. package/core/built/admin/assets/label-picker-DVnpu_IF.js +0 -1
  354. package/core/built/admin/assets/layout-B0YLh7R9.js +0 -1
  355. package/core/built/admin/assets/member-custom-fields-C8NpZYza.js +0 -1
  356. package/core/built/admin/assets/member-detail-CIVAdKXf.js +0 -1
  357. package/core/built/admin/assets/members-oLPiSkXD.js +0 -10
  358. package/core/built/admin/assets/modals-CB7v-k3T.js +0 -31
  359. package/core/built/admin/assets/newsletter-rbeDX9Wk.js +0 -1
  360. package/core/built/admin/assets/overview--4V-kx9c.js +0 -1
  361. package/core/built/admin/assets/overview-DSLv30oz.js +0 -1
  362. package/core/built/admin/assets/post-analytics-header-pU2m9ZuV.js +0 -1
  363. package/core/built/admin/assets/preferences-Ru4CRPHC.js +0 -5
  364. package/core/built/admin/assets/profile-BNb1oj7D.js +0 -1
  365. package/core/built/admin/assets/select-B0HteHrp.js +0 -1
  366. package/core/built/admin/assets/settings-B1C9Q3wo.js +0 -4
  367. package/core/built/admin/assets/social-urls-DA2lXrhW.js +0 -8
  368. package/core/built/admin/assets/switch-CyQcwMAv.js +0 -1
  369. package/core/built/admin/assets/tabs-D4y1gqUd.js +0 -1
  370. package/core/built/admin/assets/tags-Dmft-Y4O.js +0 -1
  371. package/core/built/admin/assets/toggle-group-R2QMFvXW.js +0 -1
  372. package/core/built/admin/assets/use-filter-params-WrdhAISv.js +0 -1
  373. package/core/built/admin/assets/web-BqW0b0_C.js +0 -1
  374. package/core/built/admin/assets/web-Cy5GKMWM.js +0 -2
  375. package/core/server/services/url/local-file-cache.js +0 -75
  376. package/core/server/services/url/queue.js +0 -260
  377. package/core/server/services/url/resource.js +0 -102
  378. package/core/server/services/url/resources.js +0 -480
  379. package/core/server/services/url/url-generator.js +0 -276
  380. package/core/server/services/url/url-service-facade.js +0 -366
  381. package/core/server/services/url/url-service-facade.ts +0 -519
  382. package/core/server/services/url/url-service.js +0 -387
  383. package/core/server/services/url/urls.js +0 -140
  384. package/core/shared/events/url-resource-updated-event.js +0 -33
  385. /package/core/built/admin/assets/{dist-CqO2lWS0.js → dist-CqO2lWS02.js} +0 -0
@@ -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
  }
@@ -94,7 +94,7 @@ class IndexNowPingService {
94
94
  // publishing.
95
95
  let url = null;
96
96
  try {
97
- url = this.urlService.facade.getUrlForResource({ ...post, type: 'posts' }, { absolute: true });
97
+ url = this.urlService.getUrlForResource({ ...post, type: 'posts' }, { absolute: true });
98
98
  if (!url || url.endsWith('/404/')) {
99
99
  this.logging.warn({
100
100
  event: { name: 'indexnow.unresolved_url' },
@@ -44,9 +44,7 @@ type Labs = {
44
44
  };
45
45
 
46
46
  type UrlService = {
47
- facade: {
48
- getUrlForResource(resource: Record<string, unknown>, options: {absolute: boolean}): string | null;
49
- };
47
+ getUrlForResource(resource: Record<string, unknown>, options: {absolute: boolean}): string | null;
50
48
  };
51
49
 
52
50
  type UrlUtils = {
@@ -170,7 +168,7 @@ export class IndexNowPingService {
170
168
  // publishing.
171
169
  let url: string | null = null;
172
170
  try {
173
- url = this.urlService.facade.getUrlForResource({...post, type: 'posts'}, {absolute: true});
171
+ url = this.urlService.getUrlForResource({...post, type: 'posts'}, {absolute: true});
174
172
 
175
173
  if (!url || url.endsWith('/404/')) {
176
174
  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) {
@@ -9,6 +9,8 @@ const emailAddressService = require('../email-address');
9
9
  const settingsHelpers = require('../settings-helpers');
10
10
  const EmailAddressParser = require('../email-address/email-address-parser');
11
11
  const mail = require('../mail');
12
+ const MailgunClient = require('../lib/mailgun-client');
13
+ const config = require('../../../shared/config');
12
14
  const labs = require('../../../shared/labs');
13
15
  const {Automation, EmailDesignSetting, Newsletter} = require('../../models');
14
16
  const MemberWelcomeEmailRenderer = require('./member-welcome-email-renderer');
@@ -46,7 +48,8 @@ const getSenderDetails = (designSettingsJson) => {
46
48
  };
47
49
 
48
50
  class MemberWelcomeEmailService {
49
- #mailer;
51
+ #transactionalMailer;
52
+ #bulkMailer;
50
53
  #renderer;
51
54
  #magicLinkService;
52
55
  #memberWelcomeEmails = {free: null, paid: null};
@@ -54,7 +57,8 @@ class MemberWelcomeEmailService {
54
57
 
55
58
  constructor({t, dir, singleUseTokenProvider}) {
56
59
  emailAddressService.init();
57
- this.#mailer = new mail.GhostMailer();
60
+ this.#transactionalMailer = new mail.GhostMailer();
61
+ this.#bulkMailer = new MailgunClient({config, settings: settingsCache});
58
62
  this.#renderer = new MemberWelcomeEmailRenderer({t, dir});
59
63
 
60
64
  const getSigninURL = (token) => {
@@ -306,7 +310,7 @@ class MemberWelcomeEmailService {
306
310
  logging.warn(message.text);
307
311
  }
308
312
 
309
- return this.#mailer.send({
313
+ return this.#transactionalMailer.send({
310
314
  from: fromEmail,
311
315
  subject: 'Verify email address',
312
316
  forceTextContent: true,
@@ -409,18 +413,16 @@ class MemberWelcomeEmailService {
409
413
  getSenderDetails(email.designSettings)
410
414
  );
411
415
 
412
- const headers = unsubscribe?.oneClickUrl ? {
413
- 'List-Unsubscribe': `<${unsubscribe.oneClickUrl}>`,
414
- 'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click'
415
- } : undefined;
416
-
417
416
  /** @type {string[]} */ let tags;
417
+ let sendEmail;
418
418
  switch (emailType) {
419
419
  case 'welcome':
420
420
  tags = [MEMBER_WELCOME_EMAIL_TAG];
421
+ sendEmail = this.#sendTransactionalEmail.bind(this);
421
422
  break;
422
423
  case 'automation':
423
424
  tags = [AUTOMATION_EMAIL_TAG];
425
+ sendEmail = this.#sendBulkEmail.bind(this);
424
426
  break;
425
427
  default: {
426
428
  /** @type {never} */ const _exhaustive = emailType;
@@ -430,19 +432,78 @@ class MemberWelcomeEmailService {
430
432
  }
431
433
  }
432
434
 
433
- return await this.#mailer.send({
435
+ return await sendEmail({
434
436
  to: member.email,
435
437
  subject,
436
438
  html,
437
439
  text,
440
+ tags,
441
+ trackOpens,
442
+ listUnsubscribe: unsubscribe?.oneClickUrl,
443
+ ...senderOptions
444
+ });
445
+ }
446
+
447
+ /**
448
+ * @param {object} options
449
+ * @param {string} options.to
450
+ * @param {string} options.subject
451
+ * @param {string} options.html
452
+ * @param {string} options.text
453
+ * @param {string} options.from
454
+ * @param {string} [options.replyTo]
455
+ * @param {string[]} [options.tags]
456
+ * @param {boolean} [options.trackOpens]
457
+ * @param {string} [options.listUnsubscribe]
458
+ * @returns {Promise<unknown>}
459
+ */
460
+ async #sendTransactionalEmail({to, subject, html, text, from, replyTo, tags, trackOpens, listUnsubscribe}) {
461
+ const headers = listUnsubscribe ? {
462
+ 'List-Unsubscribe': `<${listUnsubscribe}>`,
463
+ 'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click'
464
+ } : undefined;
465
+
466
+ return await this.#transactionalMailer.send({
467
+ to,
468
+ subject,
469
+ html,
470
+ text,
438
471
  forceTextContent: true,
439
472
  tags,
440
473
  ...(headers ? {headers} : {}),
441
474
  ...(typeof trackOpens === 'boolean' ? {trackOpens} : {}),
442
- ...senderOptions
475
+ from,
476
+ replyTo
443
477
  });
444
478
  }
445
479
 
480
+ /**
481
+ * @param {object} options
482
+ * @param {string} options.to
483
+ * @param {string} options.subject
484
+ * @param {string} options.html
485
+ * @param {string} options.text
486
+ * @param {string} options.from
487
+ * @param {string} [options.replyTo]
488
+ * @param {string[]} [options.tags]
489
+ * @param {boolean} [options.trackOpens]
490
+ * @param {string} [options.listUnsubscribe]
491
+ * @returns {Promise<unknown>}
492
+ */
493
+ async #sendBulkEmail({to, subject, html, text, from, replyTo, tags, trackOpens, listUnsubscribe}) {
494
+ return await this.#bulkMailer.send({
495
+ subject,
496
+ html,
497
+ plaintext: text,
498
+ from,
499
+ replyTo,
500
+ tags,
501
+ ...(typeof trackOpens === 'boolean' ? {track_opens: trackOpens} : {})
502
+ }, {
503
+ [to]: listUnsubscribe ? {list_unsubscribe: listUnsubscribe} : {}
504
+ }, []);
505
+ }
506
+
446
507
  async send({member, memberStatus}) {
447
508
  const email = this.#memberWelcomeEmails[memberStatus];
448
509
 
@@ -651,12 +712,11 @@ class MemberWelcomeEmailService {
651
712
  getSenderDetails(designSettingsJson)
652
713
  );
653
714
 
654
- await this.#mailer.send({
715
+ await this.#sendTransactionalEmail({
655
716
  to: email,
656
717
  subject: `[Test] ${renderedSubject}`,
657
718
  html,
658
719
  text,
659
- forceTextContent: true,
660
720
  ...senderOptions
661
721
  });
662
722
  }
@@ -54,7 +54,7 @@ function trimLeadingWhitespace(strings, ...values) {
54
54
 
55
55
  function createApiInstance(config) {
56
56
  const membersApiInstance = MembersApi({
57
- urlService: urlService.facade,
57
+ urlService,
58
58
  tokenConfig: config.getTokenConfig(),
59
59
  auth: {
60
60
  getSigninURL: config.getSigninURL.bind(config),
@@ -6,14 +6,18 @@ const csv_2 = require("@tryghost/custom-field-types/csv");
6
6
  const emailTemplate = require('./email-template');
7
7
  // Turn an ORM validation error into copy a member manager can act on. Presentation
8
8
  // only: the API response keeps the raw messages, and just the emailed report is
9
- // rewritten.
10
- function humaniseError(message) {
9
+ // rewritten. Takes one reason at a time, so a rewrite matching to the end of its input
10
+ // cannot consume the reason after it.
11
+ function humaniseReason(message) {
11
12
  return message
12
13
  .replace('Value in [members.email] cannot be blank.', 'Missing email address')
13
14
  .replace('Value in [members.note] exceeds maximum length of 2000 characters.', '"Note" exceeds maximum length of 2000 characters')
14
15
  .replace('Value in [members.subscribed] must be one of true, false, 0 or 1.', 'Value in "Subscribed to emails" must be "true" or "false"')
15
16
  .replace('Validation (isEmail) failed for email', 'Invalid email address')
16
- .replace(/No such customer:[^,]*/, 'Could not find Stripe customer');
17
+ .replace(/No such customer:[\s\S]*/, 'Could not find Stripe customer');
18
+ }
19
+ function humaniseError(row) {
20
+ return row.errors.map(humaniseReason).join('\n');
17
21
  }
18
22
  function stringifyLabels(labels) {
19
23
  return labels.map(label => (typeof label === 'string' ? label : label.name)).join(',');
@@ -40,7 +44,7 @@ function toErrorReportRow(row) {
40
44
  labels: stringifyLabels(row.labels),
41
45
  tiers: '',
42
46
  gift_id: row.gift_id || null,
43
- error: humaniseError(row.error)
47
+ error: humaniseError(row)
44
48
  };
45
49
  }
46
50
  // The error report attached to the completion email: the failed rows as CSV. It shares
@@ -34,14 +34,19 @@ interface EmailPayload {
34
34
 
35
35
  // Turn an ORM validation error into copy a member manager can act on. Presentation
36
36
  // only: the API response keeps the raw messages, and just the emailed report is
37
- // rewritten.
38
- function humaniseError(message: string): string {
37
+ // rewritten. Takes one reason at a time, so a rewrite matching to the end of its input
38
+ // cannot consume the reason after it.
39
+ function humaniseReason(message: string): string {
39
40
  return message
40
41
  .replace('Value in [members.email] cannot be blank.', 'Missing email address')
41
42
  .replace('Value in [members.note] exceeds maximum length of 2000 characters.', '"Note" exceeds maximum length of 2000 characters')
42
43
  .replace('Value in [members.subscribed] must be one of true, false, 0 or 1.', 'Value in "Subscribed to emails" must be "true" or "false"')
43
44
  .replace('Validation (isEmail) failed for email', 'Invalid email address')
44
- .replace(/No such customer:[^,]*/, 'Could not find Stripe customer');
45
+ .replace(/No such customer:[\s\S]*/, 'Could not find Stripe customer');
46
+ }
47
+
48
+ function humaniseError(row: ImportErrorRow): string {
49
+ return row.errors.map(humaniseReason).join('\n');
45
50
  }
46
51
 
47
52
  // One row of the fixed, member-vocabulary part of the error report, in emit order.
@@ -93,7 +98,7 @@ function toErrorReportRow(row: ImportErrorRow): ErrorReportRow {
93
98
  labels: stringifyLabels(row.labels),
94
99
  tiers: '',
95
100
  gift_id: row.gift_id || null,
96
- error: humaniseError(row.error)
101
+ error: humaniseError(row)
97
102
  };
98
103
  }
99
104
 
@@ -165,7 +165,7 @@ class MembersCSVImporter {
165
165
  // before any member write -- so there is no reason to hold a transaction
166
166
  // across it, and doing so would deadlock the single-connection SQLite pool.
167
167
  const customFieldPlan = activeCustomFields.length > 0
168
- ? await this._customFields.planWrite((0, csv_2.fieldValuesFromCsvRow)(activeCustomFields, row, csv_1.stripFormulaGuard))
168
+ ? await namingTheColumn(() => this._customFields.planWrite((0, csv_2.fieldValuesFromCsvRow)(activeCustomFields, row, csv_1.stripFormulaGuard)))
169
169
  : [];
170
170
  trx = await this._knex.transaction(undefined, { doNotRejectOnRollback: false });
171
171
  const options = { transacting: trx, context: IMPORT_CONTEXT };
@@ -265,15 +265,16 @@ class MembersCSVImporter {
265
265
  }
266
266
  catch (error) {
267
267
  const errorList = Array.isArray(error) ? error : [error];
268
- const errorMessage = errorList
268
+ const reasons = errorList
269
269
  .map(e => (typeof e === 'object' && e !== null && 'message' in e ? e.message : undefined))
270
- .join(', ');
270
+ .filter((message) => typeof message === 'string');
271
+ const errorMessage = reasons.join('\n');
271
272
  // trx is unset if the row failed before the transaction opened (a bad
272
273
  // custom field value or gift combination).
273
274
  if (trx) {
274
275
  await trx.rollback();
275
276
  }
276
- importErrors.push({ ...row, error: errorMessage });
277
+ importErrors.push({ ...row, error: errorMessage, errors: reasons });
277
278
  }
278
279
  }
279
280
  await Promise.all(archivableStripePriceIds.map(id => this._stripe.archivePrice(id)));
@@ -287,6 +288,26 @@ class MembersCSVImporter {
287
288
  return { imported: 0, errors: importErrors };
288
289
  }
289
290
  }
291
+ // A row error is read next to a spreadsheet, and a value rejection states only what the
292
+ // value should be — the same sentence Admin shows under a single input, where the field is
293
+ // already on screen. `property` is the dotted path a default export writes as its column
294
+ // header, so prefixing with it names the column a publisher has to go and fix.
295
+ //
296
+ // A rejection about the whole request carries the bare namespace and names no field after
297
+ // it, so it is left alone rather than made to point at a column that does not exist.
298
+ async function namingTheColumn(plan) {
299
+ try {
300
+ return await plan();
301
+ }
302
+ catch (error) {
303
+ const { property, message } = (error ?? {});
304
+ const namesAField = typeof property === 'string' && property.includes('.');
305
+ if (!namesAField || typeof message !== 'string') {
306
+ throw error;
307
+ }
308
+ throw new errors.DataImportError({ message: `${property}: ${message}` });
309
+ }
310
+ }
290
311
  function wrapGiftError(error) {
291
312
  const err = error;
292
313
  const message = (err && typeof err.message === 'string' && err.message) || tpl(messages.giftReassignFailed);
@@ -99,8 +99,7 @@ type CustomFieldPlan = unknown;
99
99
  // The custom fields collaborator as the import needs it. activeFields is the field set a
100
100
  // custom_fields.* column is read against, empty when the feature is off; planWrite
101
101
  // validates a row's values (throwing so the row fails whole) and applyWrite persists
102
- // them, merging a composite's sub-fields into whatever is already stored, both on the
103
- // row's transaction.
102
+ // them, touching only the parts the row named, both on the row's transaction.
104
103
  export interface CustomFieldsImport {
105
104
  activeFields(): Promise<CsvField[]>;
106
105
  planWrite(values: Record<string, unknown>): Promise<CustomFieldPlan[]>;
@@ -333,7 +332,7 @@ class MembersCSVImporter {
333
332
  // before any member write -- so there is no reason to hold a transaction
334
333
  // across it, and doing so would deadlock the single-connection SQLite pool.
335
334
  const customFieldPlan = activeCustomFields.length > 0
336
- ? await this._customFields.planWrite(fieldValuesFromCsvRow(activeCustomFields, row, stripFormulaGuard))
335
+ ? await namingTheColumn(() => this._customFields.planWrite(fieldValuesFromCsvRow(activeCustomFields, row, stripFormulaGuard)))
337
336
  : [];
338
337
 
339
338
  trx = await this._knex.transaction(undefined, {doNotRejectOnRollback: false});
@@ -433,15 +432,16 @@ class MembersCSVImporter {
433
432
  imported += 1;
434
433
  } catch (error) {
435
434
  const errorList: unknown[] = Array.isArray(error) ? error : [error];
436
- const errorMessage = errorList
435
+ const reasons = errorList
437
436
  .map(e => (typeof e === 'object' && e !== null && 'message' in e ? e.message : undefined))
438
- .join(', ');
437
+ .filter((message): message is string => typeof message === 'string');
438
+ const errorMessage = reasons.join('\n');
439
439
  // trx is unset if the row failed before the transaction opened (a bad
440
440
  // custom field value or gift combination).
441
441
  if (trx) {
442
442
  await trx.rollback();
443
443
  }
444
- importErrors.push({...row, error: errorMessage});
444
+ importErrors.push({...row, error: errorMessage, errors: reasons});
445
445
  }
446
446
  }
447
447
 
@@ -458,6 +458,26 @@ class MembersCSVImporter {
458
458
  }
459
459
  }
460
460
 
461
+ // A row error is read next to a spreadsheet, and a value rejection states only what the
462
+ // value should be — the same sentence Admin shows under a single input, where the field is
463
+ // already on screen. `property` is the dotted path a default export writes as its column
464
+ // header, so prefixing with it names the column a publisher has to go and fix.
465
+ //
466
+ // A rejection about the whole request carries the bare namespace and names no field after
467
+ // it, so it is left alone rather than made to point at a column that does not exist.
468
+ async function namingTheColumn<T>(plan: () => Promise<T>): Promise<T> {
469
+ try {
470
+ return await plan();
471
+ } catch (error) {
472
+ const {property, message} = (error ?? {}) as {property?: unknown; message?: unknown};
473
+ const namesAField = typeof property === 'string' && property.includes('.');
474
+ if (!namesAField || typeof message !== 'string') {
475
+ throw error;
476
+ }
477
+ throw new errors.DataImportError({message: `${property}: ${message}`});
478
+ }
479
+ }
480
+
461
481
  function wrapGiftError(error: unknown) {
462
482
  const err = error as {message?: unknown};
463
483
  const message = (err && typeof err.message === 'string' && err.message) || tpl(messages.giftReassignFailed);
@@ -42,7 +42,11 @@ export type MemberImportRow = z.infer<typeof memberImportRowSchema>;
42
42
 
43
43
  // A row that failed to import, carrying the raw message that stopped it. The message is
44
44
  // left raw here; turning it into human copy is a presentation concern.
45
- export type ImportErrorRow = MemberImportRow & {error: string};
45
+ // A row can fail for more than one reason. `errors` is the list; `error` is that list
46
+ // written out for the error report's single cell. Two fields because a reason may quote a
47
+ // cell the publisher wrote, and a CSV cell legally holds commas and newlines, so no
48
+ // separator survives being split back apart.
49
+ export type ImportErrorRow = MemberImportRow & {error: string; errors: string[]};
46
50
 
47
51
  // The persisted import label as plain data -- taken off the Bookshelf model before it
48
52
  // leaves the kernel, so the result and the email carry only what they show.
@@ -52,11 +52,7 @@ function makeImporter(deps) {
52
52
  const customFields = {
53
53
  activeFields: async () => (labs.isSet('membersCustomFields') ? deps.customFields.definitions.browse() : []),
54
54
  planWrite: values => deps.customFields.values.planWrite(values),
55
- // mergeComposites: a sub-field is a field, so a row writes the sub-fields it fills
56
- // and leaves the rest alone, the same rule a blank `name` column follows. Without
57
- // it a file naming one address column would replace the whole address, clearing
58
- // five sub-fields it never mentioned.
59
- applyWrite: (memberId, plan, executor) => deps.customFields.values.applyWrite(memberId, plan, executor, { mergeComposites: true })
55
+ applyWrite: (memberId, plan, executor) => deps.customFields.values.applyWrite(memberId, plan, { executor })
60
56
  };
61
57
  return new importer_1.default({
62
58
  knex: deps.knex,
@@ -33,7 +33,7 @@ interface ImporterServices {
33
33
  definitions: {browse(): Promise<CsvField[]>};
34
34
  values: {
35
35
  planWrite(values: Record<string, unknown>): Promise<unknown[]>;
36
- applyWrite(memberId: string, plan: unknown[], executor: Knex, options?: {mergeComposites?: boolean}): Promise<void>;
36
+ applyWrite(memberId: string, plan: unknown[], options?: {executor?: Knex}): Promise<void>;
37
37
  };
38
38
  };
39
39
  }
@@ -87,11 +87,7 @@ export function makeImporter(deps: ImporterServices) {
87
87
  const customFields: CustomFieldsImport = {
88
88
  activeFields: async () => (labs.isSet('membersCustomFields') ? deps.customFields.definitions.browse() : []),
89
89
  planWrite: values => deps.customFields.values.planWrite(values),
90
- // mergeComposites: a sub-field is a field, so a row writes the sub-fields it fills
91
- // and leaves the rest alone, the same rule a blank `name` column follows. Without
92
- // it a file naming one address column would replace the whole address, clearing
93
- // five sub-fields it never mentioned.
94
- applyWrite: (memberId, plan, executor) => deps.customFields.values.applyWrite(memberId, plan, executor, {mergeComposites: true})
90
+ applyWrite: (memberId, plan, executor) => deps.customFields.values.applyWrite(memberId, plan, {executor})
95
91
  };
96
92
 
97
93
  return new MembersCSVImporter({
@@ -3,19 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.activeFields = activeFields;
4
4
  const schema_1 = require("./schema");
5
5
  const FIELDS_TABLE = 'members_custom_fields';
6
- // Shared query builders over the field definitions, used by both the definitions
7
- // service and the values service.
8
- //
9
- // The `status = 'active'` filter is the one invariant worth centralising: archived
10
- // fields must stay out of every read and every write, and that's enforced by a
11
- // filter, not a database constraint — nothing stops a value row referencing an
12
- // archived field, so a query that forgets the filter is a silent bug. Keeping the
13
- // filter in one builder means there's one place to get it right.
14
- /**
15
- * The active field definitions. Takes the executor — a knex instance or a
16
- * transaction — so the same query runs standalone or inside a write's
17
- * transaction; callers chain their own `select`/`orderBy`/`whereIn` onto it.
18
- */
6
+ // Archived fields must stay out of every read and write, and nothing in the database
7
+ // enforces that no constraint stops a value row referencing an archived field. A query
8
+ // that forgets the filter is a silent bug, so the filter lives in one place.
9
+ /** Takes the executor so the same query runs standalone or inside a write's transaction. */
19
10
  function activeFields(db) {
20
11
  return db(FIELDS_TABLE).where('status', schema_1.FIELD_STATUS.active);
21
12
  }
@@ -3,20 +3,11 @@ import {FIELD_STATUS} from './schema';
3
3
 
4
4
  const FIELDS_TABLE = 'members_custom_fields';
5
5
 
6
- // Shared query builders over the field definitions, used by both the definitions
7
- // service and the values service.
8
- //
9
- // The `status = 'active'` filter is the one invariant worth centralising: archived
10
- // fields must stay out of every read and every write, and that's enforced by a
11
- // filter, not a database constraint — nothing stops a value row referencing an
12
- // archived field, so a query that forgets the filter is a silent bug. Keeping the
13
- // filter in one builder means there's one place to get it right.
6
+ // Archived fields must stay out of every read and write, and nothing in the database
7
+ // enforces that no constraint stops a value row referencing an archived field. A query
8
+ // that forgets the filter is a silent bug, so the filter lives in one place.
14
9
 
15
- /**
16
- * The active field definitions. Takes the executor — a knex instance or a
17
- * transaction — so the same query runs standalone or inside a write's
18
- * transaction; callers chain their own `select`/`orderBy`/`whereIn` onto it.
19
- */
10
+ /** Takes the executor so the same query runs standalone or inside a write's transaction. */
20
11
  export function activeFields(db: Knex) {
21
12
  return db(FIELDS_TABLE).where('status', FIELD_STATUS.active);
22
13
  }