ghost 6.55.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 (483) 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-default-cards-10.3.3.tgz +0 -0
  6. package/components/tryghost-kg-default-nodes-2.1.4.tgz +0 -0
  7. package/components/tryghost-kg-default-transforms-1.3.3.tgz +0 -0
  8. package/components/tryghost-kg-html-to-lexical-1.3.3.tgz +0 -0
  9. package/components/tryghost-kg-lexical-html-renderer-1.4.3.tgz +0 -0
  10. package/components/tryghost-nql-string-0.0.0.tgz +0 -0
  11. package/components/tryghost-parse-email-address-0.0.0.tgz +0 -0
  12. package/core/app.js +1 -1
  13. package/core/boot.js +21 -25
  14. package/core/bridge.js +10 -12
  15. package/core/built/admin/assets/Combination-BDvCdg1t.js +41 -0
  16. package/core/built/admin/assets/{PolarAngleAxis-BGjf7sBu.js → PolarAngleAxis-C4qtHy1B.js} +1 -1
  17. package/core/built/admin/assets/{RadialBarChart-zeFf9mBz.js → RadialBarChart-Bh09Stsj.js} +1 -1
  18. package/core/built/admin/assets/about-C7O-AgRZ.js +1 -0
  19. package/core/built/admin/assets/{account-migration-sjHLx5dy.js → account-migration-Cv_ol1ko.js} +1 -1
  20. package/core/built/admin/assets/action-list-lGzZLHc8.js +1 -0
  21. package/core/built/admin/assets/activitypub/{account-migration-DoYXtnZ8.js → account-migration-DHFbePl6.js} +7 -7
  22. package/core/built/admin/assets/activitypub/activitypub.js +6 -6
  23. package/core/built/admin/assets/activitypub/{avatar-B3DEDB1l.js → avatar-CT4tS2_S.js} +2 -2
  24. package/core/built/admin/assets/activitypub/{avatar-flipboard-Cqrg0XMx.js → avatar-flipboard-ByYWKipo.js} +2 -2
  25. package/core/built/admin/assets/activitypub/{bluesky-sharing-Bxeh6MKW.js → bluesky-sharing-srVnSHDz.js} +16 -16
  26. package/core/built/admin/assets/activitypub/{content-formatters-BpQARh28.js → content-formatters-C2sos5Ht.js} +3 -3
  27. package/core/built/admin/assets/activitypub/{copy-D9UWIAan.js → copy-B1UBTOoZ.js} +2 -2
  28. package/core/built/admin/assets/activitypub/{dist-D8sFfdpC.js → dist-MPsb2WZ1.js} +12 -12
  29. package/core/built/admin/assets/activitypub/{domain-CWueFW6q.js → domain-DzcM9Dzo.js} +9 -9
  30. package/core/built/admin/assets/activitypub/{edit-profile-BGxPU7N5.js → edit-profile-CsQ_4xAk.js} +945 -945
  31. package/core/built/admin/assets/activitypub/{explore-DBK-u0Ch.js → explore-CJe0DZBK.js} +8 -8
  32. package/core/built/admin/assets/activitypub/{feed-Bcir5RQZ.js → feed-CKV3qWON.js} +33 -33
  33. package/core/built/admin/assets/activitypub/{field-BV-CIT3d.js → field-DckHfDJf.js} +3 -3
  34. package/core/built/admin/assets/activitypub/{hash-BC9Q0N4c.js → hash-Dlp8RM55.js} +2 -2
  35. package/core/built/admin/assets/activitypub/{inbox-DQ4ZnC1m.js → inbox-CsPy4R0M.js} +2 -2
  36. package/core/built/admin/assets/activitypub/{inbox-yC0nfvzl.js → inbox-DAsjzUUe.js} +85 -85
  37. package/core/built/admin/assets/activitypub/{label-DTRS9TDk.js → label-DWyo9E5T.js} +3 -3
  38. package/core/built/admin/assets/activitypub/layout-DM_3pqsU.js +1 -0
  39. package/core/built/admin/assets/activitypub/{moderation-Dwl9t0pb.js → moderation-B0Inc03M.js} +11 -11
  40. package/core/built/admin/assets/activitypub/{note-B-zKsZ7r.js → note-CSE54lER.js} +7 -7
  41. package/core/built/admin/assets/activitypub/{notifications-DjJZD54t.js → notifications-RSwbKqdh.js} +9 -9
  42. package/core/built/admin/assets/activitypub/{preferences-CoU8vs6d.js → preferences-CE231n2_.js} +20 -20
  43. package/core/built/admin/assets/activitypub/{profile-BAsaJekx.js → profile-aaQ1j1Eo.js} +237 -237
  44. package/core/built/admin/assets/activitypub/{reply-CpdMeAO5.js → reply-Cjb0Vs3m.js} +2 -2
  45. package/core/built/admin/assets/activitypub/{routes-CLH3OVTP.js → routes-Djb0mc9k.js} +1139 -1150
  46. package/core/built/admin/assets/activitypub/{separator-BI1FLsYM.js → separator-D8pROI5n.js} +3 -3
  47. package/core/built/admin/assets/activitypub/{settings-DA6pNJ-Q.js → settings-DMc32IQl.js} +13 -13
  48. package/core/built/admin/assets/activitypub/{site-CQD42Pw5.js → site-DVIcsohX.js} +81 -81
  49. package/core/built/admin/assets/activitypub/{step-1-DIDg4aHb.js → step-1-9Yey9usF.js} +5 -5
  50. package/core/built/admin/assets/activitypub/{step-2-BfGe8BYt.js → step-2-DdYmKRXV.js} +6 -6
  51. package/core/built/admin/assets/activitypub/{step-3-mE9spAqw.js → step-3-hdSkcjZU.js} +10 -10
  52. package/core/built/admin/assets/activitypub/{tabs-Cg0saow1.js → tabs-CUqseYzQ.js} +317 -315
  53. package/core/built/admin/assets/activitypub/{topic-filter-DLHBZT93.js → topic-filter-Coni6OXM.js} +3 -3
  54. package/core/built/admin/assets/activitypub/{use-activity-pub-queries-VzbN99u_.js → use-activity-pub-queries-MeHTPMQv.js} +2 -2
  55. package/core/built/admin/assets/activitypub/{use-navigate-with-base-path-CAwXTmqH.js → use-navigate-with-base-path-B046AM38.js} +115 -96
  56. package/core/built/admin/assets/activitypub/{use-reply-chain-data-Br4aOWOh.js → use-reply-chain-data-1AO-Xob6.js} +5 -5
  57. package/core/built/admin/assets/activitypub/{x-C8tAnXly.js → x-BRfRYd16.js} +2 -2
  58. package/core/built/admin/assets/add-integration-modal-m9Dv6f77.js +1 -0
  59. package/core/built/admin/assets/add-newsletter-modal-CPSVZL2H.js +1 -0
  60. package/core/built/admin/assets/add-recommendation-modal-Ci9Z5yat.js +1 -0
  61. package/core/built/admin/assets/analytics-DqSCVV8U.js +1 -0
  62. package/core/built/admin/assets/announcement-bar-modal-DU3avV7d.js +1 -0
  63. package/core/built/admin/assets/api-keys-BK0LylyK.js +1 -0
  64. package/core/built/admin/assets/api-keys-_iCT4dxr.js +1 -0
  65. package/core/built/admin/assets/{audience-DjEWNhp3.js → audience-DTJtXXWY.js} +1 -1
  66. package/core/built/admin/assets/automated-emails-PayPCT8L.js +1 -0
  67. package/core/built/admin/assets/{automations-DhKeS_U5.js → automations-162q5HjL.js} +1 -1
  68. package/core/built/admin/assets/{automations-Bx3VcFIL.js → automations-dtQ_MRZ7.js} +1 -1
  69. package/core/built/admin/assets/{bluesky-sharing-ClMd0-Ft.js → bluesky-sharing-B8Es3AuV.js} +1 -1
  70. package/core/built/admin/assets/{box-BWbhXh21.js → box-By_EgGtm.js} +1 -1
  71. package/core/built/admin/assets/brand-icon-09YqFZjV.js +1 -0
  72. package/core/built/admin/assets/{breadcrumb-BD0U2ygg.js → breadcrumb-tg5zL--z.js} +1 -1
  73. package/core/built/admin/assets/build-CsiH26uS.js +1 -0
  74. package/core/built/admin/assets/{card-DGoDJzrn.js → card-aUHX0hyR.js} +1 -1
  75. package/core/built/admin/assets/{chart-BuRT2Y6p.js → chart-BnXyfH11.js} +1 -1
  76. package/core/built/admin/assets/{chart-helpers-fjQKRVyY.js → chart-helpers-CZElTGYL.js} +1 -1
  77. package/core/built/admin/assets/checkbox-Cyo-x7mC.js +1 -0
  78. package/core/built/admin/assets/chunk.524.cd5498dd4e195f679266.js +25 -0
  79. package/core/built/admin/assets/chunk.582.6f92ec1f03734bf2ea3f.js +27 -0
  80. package/core/built/admin/assets/{chunk.481.6c7395a49484bb4583aa.js → chunk.873.b8fc05a8ab877fb819ed.js} +62 -60
  81. package/core/built/admin/assets/code-editor-view-Dq1rQ4tS.js +1 -0
  82. package/core/built/admin/assets/color-picker-field-BonBgxhU.js +1 -0
  83. package/core/built/admin/assets/color-utils-BsrwXuDz.js +1 -0
  84. package/core/built/admin/assets/combobox-uX5r_paI.js +1 -0
  85. package/core/built/admin/assets/command-DxwBO2pF.js +1 -0
  86. package/core/built/admin/assets/comments-Dn-RO-nU.js +1 -0
  87. package/core/built/admin/assets/components-CwL6a6As-B0E0-NBJ.js +1 -0
  88. package/core/built/admin/assets/confirmation-modal-B6D6mk6R.js +1 -0
  89. package/core/built/admin/assets/{container-BYz2Ed39.js → container-LiZf3XdY.js} +1 -1
  90. package/core/built/admin/assets/content-api-modal-BxQ-7T6u.js +1 -0
  91. package/core/built/admin/assets/copy-field-BAxL4Zqn.js +1 -0
  92. package/core/built/admin/assets/currency-Dy4Tc2R2.js +1 -0
  93. package/core/built/admin/assets/custom-integration-modal---d_l7lc.js +1 -0
  94. package/core/built/admin/assets/{data-list-16rkJYYi.js → data-list-CupF7bjM.js} +1 -1
  95. package/core/built/admin/assets/{date-range-select-BFp58EMk.js → date-range-select-PB2n1HZf.js} +1 -1
  96. package/core/built/admin/assets/debounce-Dg2fW5Rg.js +3 -0
  97. package/core/built/admin/assets/design-and-theme-modal-DYRT207a.js +23 -0
  98. package/core/built/admin/assets/dirty-confirm-dialog-BnDJ39ao.js +1 -0
  99. package/core/built/admin/assets/disabled-sources-indicator-iMHrkyV3.js +1 -0
  100. package/core/built/admin/assets/dist-B2PvRwLn.js +1 -0
  101. package/core/built/admin/assets/{dist-DGzPJ_bO.js → dist-Cb5RSHSg.js} +1 -1
  102. package/core/built/admin/assets/{dist-P_ohUlTi.js → dist-CqO2lWS02.js} +1 -1
  103. package/core/built/admin/assets/{dist-pHiOxyxa.js → dist-D5vhMdXf.js} +1 -1
  104. package/core/built/admin/assets/dist-DlCyLrOC.js +1 -0
  105. package/core/built/admin/assets/dist-WeOIvSYM.js +23 -0
  106. package/core/built/admin/assets/{dist-DyViJwIy.js → dist-cFzKLG7y.js} +3 -3
  107. package/core/built/admin/assets/dist-j1xxxnY8.js +1 -0
  108. package/core/built/admin/assets/dist-myQaY0ka.js +1 -0
  109. package/core/built/admin/assets/domain-BR8FIdjD.js +1 -0
  110. package/core/built/admin/assets/{dropzone-DlK-K2DQ.js → dropzone-hNxmUvwD.js} +1 -1
  111. package/core/built/admin/assets/{edit-profile-BKT-REJw.js → edit-profile-CENrkY0l.js} +1 -1
  112. package/core/built/admin/assets/editor-B18D0lm8.js +8 -0
  113. package/core/built/admin/assets/embed-signup-form-modal-BsCYUMvQ.js +9 -0
  114. package/core/built/admin/assets/{empty-indicator-oX1u1Pkn.js → empty-indicator-WB1WgT9h.js} +1 -1
  115. package/core/built/admin/assets/{explore-ClUpJZOp.js → explore-BmrnljWw.js} +1 -1
  116. package/core/built/admin/assets/{feed-DF92dUMP.js → feed-Dy4li6i8.js} +1 -1
  117. package/core/built/admin/assets/{field-Do7lieBA.js → field-Dh_lwk13.js} +1 -1
  118. package/core/built/admin/assets/filter-sources-D2zDuJH5.js +1 -0
  119. package/core/built/admin/assets/filters-3JBQcNtJ.js +1 -0
  120. package/core/built/admin/assets/filters-CFt_BDe4.js +1 -0
  121. package/core/built/admin/assets/first-promoter-modal-D6Rjxugw.js +1 -0
  122. package/core/built/admin/assets/format-url-C-t8G6Qd.js +1 -0
  123. package/core/built/admin/assets/{ghost-a9c788f6f0b1ea6cf0f14ca4b13f4496.css → ghost-d38c30b866e59f2e06a357b5eb844fe5.css} +1 -1
  124. package/core/built/admin/assets/{ghost-c4057474943effc568c5659ccc4909d7.js → ghost-d683fbeb945b13155ea669caa5d8db41.js} +420 -398
  125. package/core/built/admin/assets/{ghost-dark-db36d795d21c61426837df40af270966.css → ghost-dark-1bffd5099ba7a749303133efacf8480b.css} +1 -1
  126. package/core/built/admin/assets/{gift-link-modal-f9VIMNDv.js → gift-link-modal-zMtlJhLQ.js} +1 -1
  127. package/core/built/admin/assets/{gift-links-CsUhefWG.js → gift-links-CVuqLhsZ.js} +1 -1
  128. package/core/built/admin/assets/global-data-provider-B949LDXJ.js +1 -0
  129. package/core/built/admin/assets/grid-CUciBfjf.js +1 -0
  130. package/core/built/admin/assets/{growth-CVw1My4S.js → growth-C38Xtp8D.js} +2 -2
  131. package/core/built/admin/assets/{growth-B3I6gNLm.js → growth-CFws67_i.js} +1 -1
  132. package/core/built/admin/assets/history-modal-ILAOkbxH.js +1 -0
  133. package/core/built/admin/assets/{hooks-CixO01lL.js → hooks-D6EE7BIt.js} +2 -2
  134. package/core/built/admin/assets/html-field-9a9Id_2q.js +1 -0
  135. package/core/built/admin/assets/iframe-buffering-e8vH4-Qn.js +1 -0
  136. package/core/built/admin/assets/images-CrQKVc9Q.js +1 -0
  137. package/core/built/admin/assets/{inbox-CFBSu4Ed.js → inbox-BrSRzyvz.js} +6 -6
  138. package/core/built/admin/assets/index-BHy2DZX-.js +67 -0
  139. package/core/built/admin/assets/index-Ds2iUoF6.css +1 -0
  140. package/core/built/admin/assets/index.es-B9tvf-t4.js +1 -0
  141. package/core/built/admin/assets/{inline-EMAm3okk.js → inline-Cw-gigDW.js} +1 -1
  142. package/core/built/admin/assets/input-group-6vEPvX9S.js +1 -0
  143. package/core/built/admin/assets/{input-surface-BpAjZXY5.js → input-surface-BdOe4KzO.js} +1 -1
  144. package/core/built/admin/assets/integration-header-B4DyXV06.js +1 -0
  145. package/core/built/admin/assets/integrations-BT6_iJvu.js +1 -0
  146. package/core/built/admin/assets/invite-user-modal-5xDCEw90.js +1 -0
  147. package/core/built/admin/assets/isEmail-BOfC647y.js +1 -0
  148. package/core/built/admin/assets/koenig-editor-base-BWLEB9Vf.js +1 -0
  149. package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +9535 -9527
  150. package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +124 -124
  151. package/core/built/admin/assets/{koenig-lexical-f8vHRYvz.js → koenig-lexical-Cj62r_y9.js} +86 -86
  152. package/core/built/admin/assets/{kpi-card-DSap9rAy.js → kpi-card-DOUnkZcN.js} +1 -1
  153. package/core/built/admin/assets/{kpi-card-DU0rn9Cj.js → kpi-card-IHDo8Q56.js} +1 -1
  154. package/core/built/admin/assets/{kpi-helpers-Dz33AV6h.js → kpi-helpers-CjUdECAK.js} +1 -1
  155. package/core/built/admin/assets/{kpi-tabs-BtTTrKvJ.js → kpi-tabs-X3DkOYbI.js} +1 -1
  156. package/core/built/admin/assets/{label-DS8Mi6G5.js → label-Dq6QaOuS.js} +1 -1
  157. package/core/built/admin/assets/label-picker-8YlhS2U3.js +1 -0
  158. package/core/built/admin/assets/labels-DAPjKPy2.js +1 -0
  159. package/core/built/admin/assets/layout-TMj4P-vv.js +1 -0
  160. package/core/built/admin/assets/limit-modal-BRPsit_B.js +1 -0
  161. package/core/built/admin/assets/link-helpers-Bzkk68GD.js +1 -0
  162. package/core/built/admin/assets/{list-page-B6ANtzkd.js → list-page-CdsnQR9N.js} +1 -1
  163. package/core/built/admin/assets/member-custom-fields-CmvsuuV_.js +1 -0
  164. package/core/built/admin/assets/member-detail-DCvRrkmJ.js +1 -0
  165. package/core/built/admin/assets/members-DxbSKngD.js +11 -0
  166. package/core/built/admin/assets/modal-page-BMTzFHVO.js +1 -0
  167. package/core/built/admin/assets/{moderation-DySbgyUS.js → moderation-Da5lhhWh.js} +1 -1
  168. package/core/built/admin/assets/multi-select-combobox-CYdkH7KQ.js +1 -0
  169. package/core/built/admin/assets/navigation-modal-BIoTwtMy.js +1 -0
  170. package/core/built/admin/assets/newsletter-BptlVoJm.js +1 -0
  171. package/core/built/admin/assets/newsletter-detail-modal-BIvxqE0N.js +1 -0
  172. package/core/built/admin/assets/newsletter-emails-Bf8IZLsP.js +1 -0
  173. package/core/built/admin/assets/newsletters-BgrIZk0V.js +1 -0
  174. package/core/built/admin/assets/{newsletters-BXJbJgA-.js → newsletters-Du0lzskS.js} +1 -1
  175. package/core/built/admin/assets/{note-C8kxrz6w.js → note-DYcLOmDb.js} +1 -1
  176. package/core/built/admin/assets/{notifications-CgtSfX7t.js → notifications-B2SPFIV9.js} +1 -1
  177. package/core/built/admin/assets/{offers-DEniVRD5.js → offers-Um6ab71O.js} +1 -1
  178. package/core/built/admin/assets/offers-container-modal-BZnZITMw.js +1 -0
  179. package/core/built/admin/assets/{onboarding-route-4qtziA9d.js → onboarding-route-DgdfxkQh.js} +1 -1
  180. package/core/built/admin/assets/overview-BYQJXSog.js +1 -0
  181. package/core/built/admin/assets/overview-Dt2_mR6U.js +1 -0
  182. package/core/built/admin/assets/{page-header-B3Wb9Jqv.js → page-header-CpmxJj5B.js} +1 -1
  183. package/core/built/admin/assets/{pages-B4goRPUn.js → pages-BVMnpI2O.js} +1 -1
  184. package/core/built/admin/assets/pintura-modal-B4MuBuuO.js +1 -0
  185. package/core/built/admin/assets/portal-frame-BF0NBIji.js +1 -0
  186. package/core/built/admin/assets/portal-modal-DMVbm0oy.js +1 -0
  187. package/core/built/admin/assets/{post-analytics-We-ko5Cb.js → post-analytics-CXWBOGyP.js} +1 -1
  188. package/core/built/admin/assets/post-analytics-header-ChzjXVPt.js +1 -0
  189. package/core/built/admin/assets/{post-analytics-provider-B6UynTGb.js → post-analytics-provider-Cgw4U_fw.js} +1 -1
  190. package/core/built/admin/assets/{post-share-modal-DFfO93_0.js → post-share-modal-B9Vmk_XM.js} +1 -1
  191. package/core/built/admin/assets/{posts-DhXE7b6r.js → posts-CxG7v0-M.js} +1 -1
  192. package/core/built/admin/assets/preferences-BfzJmnsd.js +5 -0
  193. package/core/built/admin/assets/preview-chrome-BOVO6_PE.js +1 -0
  194. package/core/built/admin/assets/preview-modal-Dw_HtD6S.js +1 -0
  195. package/core/built/admin/assets/profile-BT5vtv9b.js +1 -0
  196. package/core/built/admin/assets/radio-group-I7ou7PoJ.js +1 -0
  197. package/core/built/admin/assets/{referrers-pmJT4k3-.js → referrers-B9QDAfYm.js} +1 -1
  198. package/core/built/admin/assets/{router-provider-C1R4AZ2o.js → router-provider-DSQfprxH.js} +1 -1
  199. package/core/built/admin/assets/select-D9oU1IoM.js +1 -0
  200. package/core/built/admin/assets/settings-CfZFbyu_.js +4 -0
  201. package/core/built/admin/assets/{settings-WXNeWyU1.js → settings-CpvHWix7.js} +1 -1
  202. package/core/built/admin/assets/settings-breadcrumbs-K0USP2nw.js +1 -0
  203. package/core/built/admin/assets/{shade-app-B3heOAfz-CGXQwWeO.js → shade-app-B3heOAfz-BhQXxNkT.js} +1 -1
  204. package/core/built/admin/assets/{share-modal-DsgW3wbg.js → share-modal-ClIyPzc5.js} +1 -1
  205. package/core/built/admin/assets/slack-modal-1XXqOfiw.js +1 -0
  206. package/core/built/admin/assets/social-urls-CHQ9cTAK.js +1 -0
  207. package/core/built/admin/assets/{sort-button-Bb_FCl7e.js → sort-button-CdBsm40v.js} +1 -1
  208. package/core/built/admin/assets/{source-icon-BeaRxUn9.js → source-icon-CEYmVLXR.js} +1 -1
  209. package/core/built/admin/assets/{stack-xiN0eQ6Z.js → stack-CIfOAAVf.js} +1 -1
  210. package/core/built/admin/assets/{stats-Bjj_kmJ1.js → stats-D8nZCTy5.js} +1 -1
  211. package/core/built/admin/assets/{stats-view-BxPbW6K1.js → stats-view-CugFVf0g.js} +1 -1
  212. package/core/built/admin/assets/{step-1-B1e3-1F8.js → step-1-DOYjrORn.js} +1 -1
  213. package/core/built/admin/assets/{step-2-C5WaeBJo.js → step-2-vA6RsarH.js} +1 -1
  214. package/core/built/admin/assets/{step-3-B46iVPwu.js → step-3-BtIBxMId.js} +2 -2
  215. package/core/built/admin/assets/stripe-button-UFP4QRXd.js +1 -0
  216. package/core/built/admin/assets/stripe-connect-modal-C_L3uM6n.js +1 -0
  217. package/core/built/admin/assets/switch-CE2NuMn-.js +1 -0
  218. package/core/built/admin/assets/{table-CB-4_HBJ.js → table-TMCgCQK7.js} +1 -1
  219. package/core/built/admin/assets/tabs-Dd6oOKbv.js +1 -0
  220. package/core/built/admin/assets/tag-detail-D3l-UJPR.js +1 -0
  221. package/core/built/admin/assets/tags-BwCgmr5v.js +1 -0
  222. package/core/built/admin/assets/tags-WdKQGWR8.js +1 -0
  223. package/core/built/admin/assets/testimonials-modal-CqWp-xXJ.js +1 -0
  224. package/core/built/admin/assets/{text-DwtjwGkh.js → text-DU9Hv33_.js} +1 -1
  225. package/core/built/admin/assets/textarea-d9qEkMOP.js +1 -0
  226. package/core/built/admin/assets/tier-detail-modal-BkcR2u0A.js +1 -0
  227. package/core/built/admin/assets/{tiers-DYsa4DCo.js → tiers-Da6kxHQP.js} +1 -1
  228. package/core/built/admin/assets/toggle-group-Guorw93z.js +1 -0
  229. package/core/built/admin/assets/{topic-filter-DAFT87tQ.js → topic-filter-CbgoWIHn.js} +1 -1
  230. package/core/built/admin/assets/transistor-modal-DuNnnpal.js +1 -0
  231. package/core/built/admin/assets/unsplash-modal-y2bVwTHH.js +1 -0
  232. package/core/built/admin/assets/{use-analytics-data-S0FTd43s.js → use-analytics-data-B-b4wNSC.js} +1 -1
  233. package/core/built/admin/assets/use-check-theme-limit-error-m09J38Mq.js +2 -0
  234. package/core/built/admin/assets/use-feature-flag-ud0Qz8vt.js +1 -0
  235. package/core/built/admin/assets/{use-filter-params-Gpoq2E1L.js → use-filter-params-BnbBDc7T.js} +1 -1
  236. package/core/built/admin/assets/use-filterable-api-iMpDZBxD.js +1 -0
  237. package/core/built/admin/assets/use-form-vRjI0Kwk.js +1 -0
  238. package/core/built/admin/assets/{use-growth-stats-CIaiHOVb.js → use-growth-stats-DPU5rt3W.js} +1 -1
  239. package/core/built/admin/assets/use-hash-link-navigation-guard-B9j6HIrV.js +1 -0
  240. package/core/built/admin/assets/use-limiter-BkN3E4gw.js +2 -0
  241. package/core/built/admin/assets/use-pintura-config-C_z8HTG2.js +1 -0
  242. package/core/built/admin/assets/use-pintura-editor-DNgbkhGp.js +1 -0
  243. package/core/built/admin/assets/{use-post-referrers-Cq2vBPrA.js → use-post-referrers-C2mXyrp-.js} +1 -1
  244. package/core/built/admin/assets/use-query-params-DHhhu8wi.js +1 -0
  245. package/core/built/admin/assets/{use-reply-chain-data-DBDRtGu-.js → use-reply-chain-data-DaK8q2qQ.js} +1 -1
  246. package/core/built/admin/assets/use-setting-group-YlYcoNyP.js +1 -0
  247. package/core/built/admin/assets/use-settings-navigation-Bf1g1rEh.js +1 -0
  248. package/core/built/admin/assets/{use-simple-pagination-CjYNxdwe.js → use-simple-pagination-D9ckKo5-.js} +1 -1
  249. package/core/built/admin/assets/{use-tinybird-query-falHAtuK.js → use-tinybird-query-GSa32hVw.js} +1 -1
  250. package/core/built/admin/assets/{use-tinybird-token-D7-8ND1y.js → use-tinybird-token-CKLOZFsw.js} +1 -1
  251. package/core/built/admin/assets/use-url-input-z-p0Kewm.js +5 -0
  252. package/core/built/admin/assets/user-detail-modal-BMxOA4Sv.js +1 -0
  253. package/core/built/admin/assets/{users-Dkv8NQZe.js → users-B6hIds6n.js} +1 -1
  254. package/core/built/admin/assets/validator-Cr0sMKuW.js +7 -0
  255. package/core/built/admin/assets/{virtual-list-CvnC9zF2.js → virtual-list-RHeEL05L.js} +1 -1
  256. package/core/built/admin/assets/{web-jFYWCKDn.js → web-Bw2QMO5u.js} +2 -2
  257. package/core/built/admin/assets/web-xJtcwMy4.js +1 -0
  258. package/core/built/admin/assets/zapier-modal-BP2f4-vY.js +1 -0
  259. package/core/built/admin/index.html +20 -18
  260. package/core/frontend/helpers/authors.js +1 -1
  261. package/core/frontend/helpers/link.js +3 -8
  262. package/core/frontend/helpers/tags.js +1 -1
  263. package/core/frontend/meta/author-url.js +2 -2
  264. package/core/frontend/meta/url.js +4 -4
  265. package/core/frontend/services/data/fetch-data.js +10 -5
  266. package/core/frontend/services/proxy.js +1 -1
  267. package/core/frontend/services/routing/api-adapter.js +115 -0
  268. package/core/frontend/services/routing/api-adapter.ts +157 -0
  269. package/core/frontend/services/routing/collection-router.js +5 -3
  270. package/core/frontend/services/routing/controllers/index.js +1 -1
  271. package/core/frontend/services/routing/controllers/rss.js +26 -31
  272. package/core/frontend/services/routing/controllers/rss.ts +66 -0
  273. package/core/frontend/services/routing/controllers/static.js +8 -7
  274. package/core/frontend/services/routing/events.js +20 -6
  275. package/core/frontend/services/routing/parent-router.js +17 -4
  276. package/core/frontend/services/routing/permalink-adapter.js +13 -0
  277. package/core/frontend/services/routing/permalink-adapter.ts +10 -0
  278. package/core/frontend/services/routing/registry.js +59 -33
  279. package/core/frontend/services/routing/router-manager.js +24 -22
  280. package/core/frontend/services/routing/static-routes-router.js +4 -4
  281. package/core/frontend/services/routing/taxonomy-router.js +6 -2
  282. package/core/frontend/services/sitemap/base-site-map-generator.js +1 -28
  283. package/core/frontend/services/sitemap/site-map-manager.js +22 -57
  284. package/core/server/adapters/route-settings/utils.js +10 -2
  285. package/core/server/adapters/route-settings/utils.ts +12 -2
  286. package/core/server/api/endpoints/email-post.js +1 -1
  287. package/core/server/api/endpoints/previews.js +1 -1
  288. package/core/server/api/endpoints/search-index-public.js +3 -3
  289. package/core/server/api/endpoints/search-index.js +4 -4
  290. package/core/server/api/endpoints/utils/serializers/input/authors.js +2 -2
  291. package/core/server/api/endpoints/utils/serializers/input/comments.js +1 -1
  292. package/core/server/api/endpoints/utils/serializers/input/pages.js +3 -3
  293. package/core/server/api/endpoints/utils/serializers/input/posts.js +3 -3
  294. package/core/server/api/endpoints/utils/serializers/input/tags.js +1 -1
  295. package/core/server/api/endpoints/utils/serializers/input/users.js +2 -2
  296. package/core/server/api/endpoints/utils/serializers/input/utils/url.js +31 -13
  297. package/core/server/api/endpoints/utils/serializers/output/mappers/posts.js +2 -2
  298. package/core/server/api/endpoints/utils/serializers/output/mappers/tags.js +2 -2
  299. package/core/server/api/endpoints/utils/serializers/output/mappers/users.js +8 -0
  300. package/core/server/api/endpoints/utils/serializers/output/utils/url.js +18 -11
  301. package/core/server/data/migrations/versions/6.56/2026-08-03-19-27-51-add-automation-run-step-id-to-automated-email-recipients.js +8 -0
  302. package/core/server/data/migrations/versions/6.56/2026-08-04-14-42-39-restrict-notification-add-permission-to-administrators.js +12 -0
  303. package/core/server/data/migrations/versions/6.57/2026-08-04-18-00-00-store-custom-field-values-as-leaf-rows.js +74 -0
  304. package/core/server/data/migrations/versions/6.57/2026-08-04-18-54-14-reset-automation-email-analytics.js +25 -0
  305. package/core/server/data/schema/commands.js +21 -5
  306. package/core/server/data/schema/fixtures/fixtures.json +2 -2
  307. package/core/server/data/schema/schema.js +25 -5
  308. package/core/server/lib/common/to-plain.ts +1 -1
  309. package/core/server/models/post.js +12 -3
  310. package/core/server/services/adapter-manager/adapter-manager.js +1 -45
  311. package/core/server/services/adapter-manager/adapter-manager.ts +2 -48
  312. package/core/server/services/adapter-manager/adapter-paths.js +24 -0
  313. package/core/server/services/adapter-manager/adapter-paths.ts +21 -0
  314. package/core/server/services/adapter-manager/base-classes.js +22 -0
  315. package/core/server/services/adapter-manager/base-classes.ts +22 -0
  316. package/core/server/services/adapter-manager/index.js +4 -27
  317. package/core/server/services/adapter-manager/index.ts +4 -31
  318. package/core/server/services/adapter-manager/utils.js +50 -0
  319. package/core/server/services/adapter-manager/utils.ts +57 -0
  320. package/core/server/services/audience-feedback/audience-feedback-controller.js +1 -1
  321. package/core/server/services/audience-feedback/audience-feedback-service.js +2 -2
  322. package/core/server/services/automations/automations-repository.ts +3 -1
  323. package/core/server/services/automations/database-automations-repository.js +33 -14
  324. package/core/server/services/automations/database-automations-repository.ts +41 -15
  325. package/core/server/services/automations/get-scheduler-idempotency-key.js +13 -0
  326. package/core/server/services/automations/get-scheduler-idempotency-key.ts +8 -0
  327. package/core/server/services/automations/poll.js +3 -1
  328. package/core/server/services/automations/poll.ts +4 -1
  329. package/core/server/services/automations/service.js +7 -3
  330. package/core/server/services/automations/service.ts +7 -3
  331. package/core/server/services/comments/comments-service-emails.js +1 -1
  332. package/core/server/services/comments/comments-service.js +1 -1
  333. package/core/server/services/email-analytics/email-analytics-service-wrapper.js +11 -2
  334. package/core/server/services/email-analytics/email-analytics-service.js +1 -1
  335. package/core/server/services/email-service/email-service-wrapper.js +2 -1
  336. package/core/server/services/email-service/mailgun-email-provider.js +15 -0
  337. package/core/server/services/gift-links/CONTEXT.md +9 -0
  338. package/core/server/services/gifts/CONTEXT.md +55 -0
  339. package/core/server/services/gifts/README.md +28 -0
  340. package/core/server/services/gifts/gift-bookshelf-repository.js +65 -45
  341. package/core/server/services/gifts/gift-bookshelf-repository.ts +203 -74
  342. package/core/server/services/gifts/gift-checkout-adapter.js +59 -0
  343. package/core/server/services/gifts/gift-checkout-offer.js +74 -0
  344. package/core/server/services/gifts/gift-checkout-offer.ts +127 -0
  345. package/core/server/services/gifts/gift-codec.js +63 -0
  346. package/core/server/services/gifts/gift-codec.ts +62 -0
  347. package/core/server/services/gifts/gift-controller.js +6 -31
  348. package/core/server/services/gifts/gift-controller.ts +6 -69
  349. package/core/server/services/gifts/gift-email-service.ts +3 -2
  350. package/core/server/services/gifts/gift-schema.js +34 -0
  351. package/core/server/services/gifts/gift-schema.ts +39 -0
  352. package/core/server/services/gifts/gift-service-wrapper.js +13 -11
  353. package/core/server/services/gifts/gift-service.js +265 -52
  354. package/core/server/services/gifts/gift-service.ts +431 -97
  355. package/core/server/services/gifts/gift.ts +16 -39
  356. package/core/server/services/indexnow-ping/indexnow-ping-service.js +1 -1
  357. package/core/server/services/indexnow-ping/indexnow-ping-service.ts +2 -4
  358. package/core/server/services/lib/mailgun-client.js +3 -3
  359. package/core/server/services/link-tracking/link-click-repository.js +6 -1
  360. package/core/server/services/link-tracking/link-click-tracking-service.js +12 -3
  361. package/core/server/services/media-inliner/external-media-inliner.js +12 -8
  362. package/core/server/services/member-attribution/url-translator.js +5 -7
  363. package/core/server/services/member-welcome-emails/member-welcome-email-renderer.js +4 -3
  364. package/core/server/services/member-welcome-emails/service.js +79 -15
  365. package/core/server/services/members/api.js +2 -3
  366. package/core/server/services/members/import-export/import/completion-email.js +8 -4
  367. package/core/server/services/members/import-export/import/completion-email.ts +9 -4
  368. package/core/server/services/members/import-export/import/importer.js +25 -4
  369. package/core/server/services/members/import-export/import/importer.ts +27 -6
  370. package/core/server/services/members/import-export/import/row.ts +5 -1
  371. package/core/server/services/members/import-export/index.js +6 -2
  372. package/core/server/services/members/import-export/index.ts +10 -4
  373. package/core/server/services/members/members-api/controllers/router-controller.js +38 -36
  374. package/core/server/services/members/members-api/members-api.js +13 -7
  375. package/core/server/services/members/members-api/repositories/event-repository.js +4 -94
  376. package/core/server/services/members/members-api/services/member-bread-service.js +3 -3
  377. package/core/server/services/members/members-api/services/payments-service.js +6 -68
  378. package/core/server/services/members-custom-fields/queries.js +4 -13
  379. package/core/server/services/members-custom-fields/queries.ts +4 -13
  380. package/core/server/services/members-custom-fields/schema.js +19 -21
  381. package/core/server/services/members-custom-fields/schema.ts +18 -20
  382. package/core/server/services/members-custom-fields/storage.js +94 -27
  383. package/core/server/services/members-custom-fields/storage.ts +114 -39
  384. package/core/server/services/members-custom-fields/values-service.js +98 -103
  385. package/core/server/services/members-custom-fields/values-service.ts +116 -114
  386. package/core/server/services/mentions/resource-service.js +1 -1
  387. package/core/server/services/mentions/service.js +2 -2
  388. package/core/server/services/oembed/oembed-service.js +44 -3
  389. package/core/server/services/posts/posts-service.js +11 -0
  390. package/core/server/services/route-settings/dynamic-routing-service.js +10 -62
  391. package/core/server/services/route-settings/route-settings-parser.js +25 -25
  392. package/core/server/services/route-settings/route-settings-parser.ts +27 -27
  393. package/core/server/services/route-settings/validation-errors.js +68 -4
  394. package/core/server/services/route-settings/validation-errors.ts +79 -4
  395. package/core/server/services/slack-ping/slack-ping-service.js +2 -2
  396. package/core/server/services/slack-ping/slack-ping-service.ts +3 -5
  397. package/core/server/services/stats/content-stats-service.js +3 -3
  398. package/core/server/services/stats/posts-stats-service.js +29 -25
  399. package/core/server/services/stripe/services/webhook/charge-refunded-event-service.js +1 -1
  400. package/core/server/services/stripe/services/webhook/checkout-session-event-service.js +16 -4
  401. package/core/server/services/stripe/stripe-api.js +10 -0
  402. package/core/server/services/url/config.js +90 -135
  403. package/core/server/services/url/index.js +6 -49
  404. package/core/server/services/url/lazy-find-resource.js +14 -11
  405. package/core/server/services/url/lazy-find-resource.ts +14 -11
  406. package/core/server/services/url/lazy-url-service.js +174 -94
  407. package/core/server/services/url/lazy-url-service.ts +233 -97
  408. package/core/server/services/url/routable-resources.js +62 -71
  409. package/core/server/services/url/router-filter.js +12 -4
  410. package/core/server/services/url/router-filter.ts +12 -4
  411. package/core/server/services/webhooks/index.js +1 -1
  412. package/core/server/services/webhooks/serialize.js +2 -1
  413. package/core/server/web/gift-preview/controller.js +10 -25
  414. package/core/shared/config/env/config.testing-mysql.json +1 -2
  415. package/core/shared/config/env/config.testing.json +1 -2
  416. package/core/shared/events/index.js +1 -2
  417. package/core/shared/labs.js +4 -4
  418. package/package.json +10 -10
  419. package/pnpm-lock.yaml +27 -20
  420. package/pnpm-workspace.yaml +3 -2
  421. package/tsconfig.json +1 -0
  422. package/components/tryghost-kg-default-nodes-2.1.3.tgz +0 -0
  423. package/core/built/admin/assets/Combination-CJG7Flp-.js +0 -41
  424. package/core/built/admin/assets/activitypub/layout-qx-bUL1k.js +0 -1
  425. package/core/built/admin/assets/automated-emails-DRy7qY-J.js +0 -7
  426. package/core/built/admin/assets/checkbox-CWa28lgO.js +0 -1
  427. package/core/built/admin/assets/chunk.524.cffd399783a519e404a9.js +0 -25
  428. package/core/built/admin/assets/chunk.582.eebf0d0b2d202bb746a5.js +0 -27
  429. package/core/built/admin/assets/code-editor-view-CCr0rBWS.js +0 -1
  430. package/core/built/admin/assets/comments-DyUSMUdj.js +0 -1
  431. package/core/built/admin/assets/disabled-sources-indicator-CUxMfiPa.js +0 -1
  432. package/core/built/admin/assets/dist-2Tnz2Yfq.js +0 -1
  433. package/core/built/admin/assets/dist-CjY-f5Z1.js +0 -1
  434. package/core/built/admin/assets/dist-DDY6GRVz.js +0 -1
  435. package/core/built/admin/assets/dist-DG78yroX.js +0 -23
  436. package/core/built/admin/assets/domain-C0bV2UV8.js +0 -1
  437. package/core/built/admin/assets/editor-Dp865O-4.js +0 -8
  438. package/core/built/admin/assets/esm-CFX93H5M.js +0 -2
  439. package/core/built/admin/assets/filter-sources-BV2rnpfA.js +0 -1
  440. package/core/built/admin/assets/filters-CUXJ2Nm9.js +0 -1
  441. package/core/built/admin/assets/filters-DfbIWc_l.js +0 -1
  442. package/core/built/admin/assets/index-9Cw8z3rA.css +0 -1
  443. package/core/built/admin/assets/index-BGG7NckO.js +0 -67
  444. package/core/built/admin/assets/input-group-BSgG_lVw.js +0 -1
  445. package/core/built/admin/assets/label-picker-1DvSuLA1.js +0 -1
  446. package/core/built/admin/assets/labels-CDSUt4J_.js +0 -1
  447. package/core/built/admin/assets/layout-yQEi2qDM.js +0 -1
  448. package/core/built/admin/assets/link-helpers-7EYqvvHs.js +0 -1
  449. package/core/built/admin/assets/member-custom-fields-yOMHyAg9.js +0 -1
  450. package/core/built/admin/assets/member-detail-BAq7gDaa.js +0 -1
  451. package/core/built/admin/assets/members-CQpVFJu4.js +0 -10
  452. package/core/built/admin/assets/modals-ReORvWnu.js +0 -31
  453. package/core/built/admin/assets/newsletter-CJQvtHqN.js +0 -1
  454. package/core/built/admin/assets/newsletters-B3QQXfGT.js +0 -1
  455. package/core/built/admin/assets/nql-BsgFMd93.js +0 -1
  456. package/core/built/admin/assets/overview-Bf2sbznq.js +0 -1
  457. package/core/built/admin/assets/overview-CBfePxcr.js +0 -1
  458. package/core/built/admin/assets/post-analytics-header-Bmc9su6q.js +0 -1
  459. package/core/built/admin/assets/preferences-DJPfHEGu.js +0 -5
  460. package/core/built/admin/assets/profile-DqBQp-gT.js +0 -1
  461. package/core/built/admin/assets/select-C5_xEt5b.js +0 -1
  462. package/core/built/admin/assets/settings-BwiVIXau.js +0 -4
  463. package/core/built/admin/assets/social-urls-BQPFEk1_.js +0 -8
  464. package/core/built/admin/assets/switch-z5Y6Uzt6.js +0 -1
  465. package/core/built/admin/assets/tabs-CCEgKYzq.js +0 -1
  466. package/core/built/admin/assets/tags-Cg2VJGmG.js +0 -1
  467. package/core/built/admin/assets/textarea-Bjccvh_E.js +0 -1
  468. package/core/built/admin/assets/toggle-group-b3IlxvhV.js +0 -1
  469. package/core/built/admin/assets/web-B_5GZRvH.js +0 -1
  470. package/core/server/services/gifts/gift-repository.js +0 -2
  471. package/core/server/services/gifts/gift-repository.ts +0 -29
  472. package/core/server/services/route-settings/activation-bridge.js +0 -174
  473. package/core/server/services/route-settings/activation-bridge.ts +0 -250
  474. package/core/server/services/url/local-file-cache.js +0 -75
  475. package/core/server/services/url/queue.js +0 -260
  476. package/core/server/services/url/resource.js +0 -102
  477. package/core/server/services/url/resources.js +0 -480
  478. package/core/server/services/url/url-generator.js +0 -276
  479. package/core/server/services/url/url-service-facade.js +0 -366
  480. package/core/server/services/url/url-service-facade.ts +0 -519
  481. package/core/server/services/url/url-service.js +0 -387
  482. package/core/server/services/url/urls.js +0 -140
  483. package/core/shared/events/url-resource-updated-event.js +0 -33
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import {createRequire} from 'node:module';
4
+ import errors from '@tryghost/errors';
4
5
  import type {AdapterConstructor} from "./types";
5
6
  import type {ConfigInstance} from '../../../shared/config/loader';
6
7
 
@@ -122,6 +123,62 @@ export function resolveAdapterExport(moduleExport: unknown): AdapterConstructor
122
123
  return null;
123
124
  }
124
125
 
126
+ /**
127
+ * Locate and load an adapter constructor by type and class name, trying each of
128
+ * `pathsToAdapters` in order. Shared by the adapter manager and by
129
+ * bin/validate-adapters.ts, so a name resolves — and fails — identically at build
130
+ * time and at runtime.
131
+ */
132
+ export function loadAdapterClass(
133
+ adapterType: string,
134
+ adapterClassName: string,
135
+ pathsToAdapters: string[],
136
+ loadAdapterFromPath: (path: string) => unknown
137
+ ): AdapterConstructor {
138
+ for (const pathToAdapters of pathsToAdapters) {
139
+ let pathToAdapter = path.join(pathToAdapters, adapterType, adapterClassName);
140
+ if (pathToAdapters === '') {
141
+ // We are loading from node_modules, we can remove the `adapterType` prefix
142
+ pathToAdapter = path.join(pathToAdapters, adapterClassName);
143
+ }
144
+ // An adapter directory may be a full module with its own
145
+ // package.json, in which case this resolves the `exports` entry
146
+ // point; otherwise the path passes through unchanged.
147
+ const moduleToLoad = resolveAdapterEntryPoint(pathToAdapter);
148
+
149
+ try {
150
+ const Adapter = resolveAdapterExport(loadAdapterFromPath(moduleToLoad));
151
+ if (Adapter) {
152
+ return Adapter;
153
+ }
154
+ } catch (err) {
155
+ // Catch runtime errors
156
+ if (!(err instanceof Error) || (err as NodeJS.ErrnoException).code !== 'MODULE_NOT_FOUND') {
157
+ throw new errors.IncorrectUsageError({err: err as Error});
158
+ }
159
+
160
+ // Catch missing dependencies BUT NOT missing adapter.
161
+ // Only check the first line — Node appends a "Require stack"
162
+ // that includes the adapter's own path, which would false-positive.
163
+ const firstLine = err.message.split('\n')[0];
164
+ if (!firstLine.includes(pathToAdapter)) {
165
+ // Name the unresolved module so the error is actionable, e.g.
166
+ // "Cannot find module 'superagent'" -> 'superagent'.
167
+ const missingMatch = /Cannot find module '([^']+)'/.exec(firstLine);
168
+ const missingModule = missingMatch ? ` '${missingMatch[1]}'` : '';
169
+ throw new errors.IncorrectUsageError({
170
+ message: `You are missing a dependency${missingModule} in your adapter ${pathToAdapter}`,
171
+ err: err as Error
172
+ });
173
+ }
174
+ }
175
+ }
176
+
177
+ throw new errors.IncorrectUsageError({
178
+ message: `Unable to find ${adapterType} adapter ${adapterClassName} in ${pathsToAdapters}.`
179
+ });
180
+ }
181
+
125
182
  function getSchedulingConfig(config: ConfigInstance) {
126
183
  const adapterConfig = config.get('adapters:scheduling');
127
184
  if (!adapterConfig) {
@@ -50,7 +50,7 @@ class AudienceFeedbackController {
50
50
  // Load the full post (with tags/authors) rather than passing an
51
51
  // id-only resource: the URL service needs status to apply the
52
52
  // published base filter and tags/authors to evaluate filtered
53
- // routes — the lazy backend rejects a bare {id} as thin.
53
+ // routes — the URL service reports a bare {id} as thin.
54
54
  const post = await this.#repository.getPostById(postId, {withRelated: ['tags', 'authors']});
55
55
  const target = post
56
56
  ? this.#audienceFeedbackService.buildLink(uuid, post, score, key)
@@ -24,7 +24,7 @@ class AudienceFeedbackService {
24
24
  */
25
25
  buildLink(uuid, post, score, key) {
26
26
  const postData = toPlain(post);
27
- let postUrl = this.#urlService.facade.getUrlForResource({...postData, type: 'posts'}, {absolute: true});
27
+ let postUrl = this.#urlService.getUrlForResource({...postData, type: 'posts'}, {absolute: true});
28
28
 
29
29
  if (postUrl.match(/\/404\//)) {
30
30
  postUrl = this.#baseURL;
@@ -36,7 +36,7 @@ class AudienceFeedbackService {
36
36
  * Feedback link for a post that no longer exists. Goes straight to the
37
37
  * home page — the same destination buildLink picks when the URL service
38
38
  * has no URL for the post — without asking the URL service: an id-only
39
- * resource can't be routed (the lazy backend rejects it as thin).
39
+ * resource can't be routed (the URL service reports it as thin).
40
40
  *
41
41
  * @param {string} uuid
42
42
  * @param {string} postId
@@ -90,6 +90,7 @@ export type AutomatedEmailEvents = {
90
90
 
91
91
  export type RecordEmailSentOptions = Readonly<{
92
92
  automationActionRevisionId: string;
93
+ automationRunStepId: string;
93
94
  mailgunMessageId?: string;
94
95
  memberEmail: string;
95
96
  memberId: string;
@@ -203,11 +204,12 @@ export interface AutomationsRepository {
203
204
  eventsByAutomatedEmailRecipientId: ReadonlyDeep<Map<string, AutomatedEmailEvents>>
204
205
  ): Promise<void>;
205
206
  /**
206
- * Track the first click for an automated email recipient.
207
+ * Record the first click timestamp for the automated email recipient identified by a run step.
207
208
  *
208
209
  */
209
210
  trackEmailClicked(options: {
210
211
  automationActionRevisionId: string;
212
+ automationRunStepId: string;
211
213
  memberId: string;
212
214
  clickedAt: Readonly<Date>;
213
215
  }, transactionOptions?: {
@@ -135,6 +135,7 @@ function createDatabaseAutomationsRepository({ knex, fakeWaitHoursMultiplier })
135
135
  member_email: options.memberEmail,
136
136
  member_name: options.memberName,
137
137
  automation_action_revision_id: options.automationActionRevisionId,
138
+ automation_run_step_id: options.automationRunStepId,
138
139
  ...(options.mailgunMessageId ? { mailgun_message_id: options.mailgunMessageId } : {}),
139
140
  track_clicks: options.trackClicks,
140
141
  track_opens: options.trackOpens,
@@ -157,6 +158,13 @@ function createDatabaseAutomationsRepository({ knex, fakeWaitHoursMultiplier })
157
158
  return;
158
159
  }
159
160
  await knex.transaction(async (trx) => {
161
+ const revisionIds = new Set();
162
+ for (const { openedAt, automationActionRevisionId } of eventsByAutomatedEmailRecipientId.values()) {
163
+ if (openedAt) {
164
+ revisionIds.add(automationActionRevisionId);
165
+ }
166
+ }
167
+ const orderedRevisionIds = await lockActionRevisions(trx, revisionIds);
160
168
  const notYetOpened = await lockNotYetOpened(trx, eventsByAutomatedEmailRecipientId);
161
169
  const newOpensPerRevision = new Map();
162
170
  for (const [id, { deliveredAt, openedAt, automationActionRevisionId }] of eventsByAutomatedEmailRecipientId) {
@@ -177,10 +185,11 @@ function createDatabaseAutomationsRepository({ knex, fakeWaitHoursMultiplier })
177
185
  newOpensPerRevision.set(automationActionRevisionId, (newOpensPerRevision.get(automationActionRevisionId) ?? 0) + 1);
178
186
  }
179
187
  }
180
- // Keep lock acquisition order consistent across concurrent transactions to avoid deadlocks.
181
- const revisions = [...newOpensPerRevision.entries()]
182
- .sort(([left], [right]) => left.localeCompare(right));
183
- for (const [id, opens] of revisions) {
188
+ for (const id of orderedRevisionIds) {
189
+ const opens = newOpensPerRevision.get(id);
190
+ if (!opens) {
191
+ continue;
192
+ }
184
193
  await trx('automation_action_revisions')
185
194
  .where({ id })
186
195
  .update({
@@ -189,24 +198,17 @@ function createDatabaseAutomationsRepository({ knex, fakeWaitHoursMultiplier })
189
198
  }
190
199
  });
191
200
  },
192
- async trackEmailClicked({ automationActionRevisionId, memberId, clickedAt }, { transacting } = {}) {
201
+ async trackEmailClicked({ automationActionRevisionId, automationRunStepId, memberId, clickedAt }, { transacting } = {}) {
193
202
  const trackClick = async (trx) => {
194
- // Match recordEmailSent's lock order to avoid deadlocks.
195
- await trx('automation_action_revisions')
196
- .select('id')
197
- .where({ id: automationActionRevisionId })
198
- .forUpdate()
199
- .first();
203
+ await lockActionRevisions(trx, [automationActionRevisionId]);
200
204
  const recipient = await trx('automated_email_recipients')
201
205
  .select('id', 'clicked_at')
202
206
  .where({
203
207
  automation_action_revision_id: automationActionRevisionId,
208
+ automation_run_step_id: automationRunStepId,
204
209
  member_id: memberId,
205
210
  track_clicks: true
206
211
  })
207
- .where('created_at', '<=', (0, database_date_1.toDatabaseDate)(clickedAt))
208
- .orderBy('created_at', 'desc')
209
- .orderBy('id', 'desc')
210
212
  .forUpdate()
211
213
  .first();
212
214
  if (!recipient || recipient.clicked_at !== null) {
@@ -233,6 +235,23 @@ function createDatabaseAutomationsRepository({ knex, fakeWaitHoursMultiplier })
233
235
  }
234
236
  };
235
237
  }
238
+ /**
239
+ * Lock revisions before recipients because inserting a recipient takes a shared
240
+ * foreign-key lock on its revision. Updating that revision later can deadlock
241
+ * with another transaction that has already locked the revision and is waiting
242
+ * for the recipient. Keep multi-revision lock acquisition deterministic.
243
+ */
244
+ async function lockActionRevisions(trx, revisionIds) {
245
+ const sortedRevisionIds = [...new Set(revisionIds)]
246
+ .sort((left, right) => left.localeCompare(right));
247
+ if (sortedRevisionIds.length > 0) {
248
+ await trx('automation_action_revisions')
249
+ .select('id')
250
+ .whereIn('id', sortedRevisionIds)
251
+ .forUpdate();
252
+ }
253
+ return sortedRevisionIds;
254
+ }
236
255
  /**
237
256
  * Which of these recipients have yet to open, and so should count towards their
238
257
  * revision's open count. Locks them for the transaction, so a worker racing on
@@ -276,6 +276,7 @@ export function createDatabaseAutomationsRepository({
276
276
  member_email: options.memberEmail,
277
277
  member_name: options.memberName,
278
278
  automation_action_revision_id: options.automationActionRevisionId,
279
+ automation_run_step_id: options.automationRunStepId,
279
280
  ...(options.mailgunMessageId ? {mailgun_message_id: options.mailgunMessageId} : {}),
280
281
  track_clicks: options.trackClicks,
281
282
  track_opens: options.trackOpens,
@@ -301,6 +302,15 @@ export function createDatabaseAutomationsRepository({
301
302
  }
302
303
 
303
304
  await knex.transaction(async (trx) => {
305
+ const revisionIds = new Set<string>();
306
+ for (const {openedAt, automationActionRevisionId} of eventsByAutomatedEmailRecipientId.values()) {
307
+ if (openedAt) {
308
+ revisionIds.add(automationActionRevisionId);
309
+ }
310
+ }
311
+
312
+ const orderedRevisionIds = await lockActionRevisions(trx, revisionIds);
313
+
304
314
  const notYetOpened = await lockNotYetOpened(trx, eventsByAutomatedEmailRecipientId);
305
315
  const newOpensPerRevision = new Map<string, number>();
306
316
 
@@ -327,11 +337,11 @@ export function createDatabaseAutomationsRepository({
327
337
  }
328
338
  }
329
339
 
330
- // Keep lock acquisition order consistent across concurrent transactions to avoid deadlocks.
331
- const revisions = [...newOpensPerRevision.entries()]
332
- .sort(([left], [right]) => left.localeCompare(right));
333
-
334
- for (const [id, opens] of revisions) {
340
+ for (const id of orderedRevisionIds) {
341
+ const opens = newOpensPerRevision.get(id);
342
+ if (!opens) {
343
+ continue;
344
+ }
335
345
  await trx('automation_action_revisions')
336
346
  .where({id})
337
347
  .update({
@@ -341,25 +351,18 @@ export function createDatabaseAutomationsRepository({
341
351
  });
342
352
  },
343
353
 
344
- async trackEmailClicked({automationActionRevisionId, memberId, clickedAt}, {transacting} = {}) {
354
+ async trackEmailClicked({automationActionRevisionId, automationRunStepId, memberId, clickedAt}, {transacting} = {}) {
345
355
  const trackClick = async (trx: Knex.Transaction) => {
346
- // Match recordEmailSent's lock order to avoid deadlocks.
347
- await trx('automation_action_revisions')
348
- .select('id')
349
- .where({id: automationActionRevisionId})
350
- .forUpdate()
351
- .first();
356
+ await lockActionRevisions(trx, [automationActionRevisionId]);
352
357
 
353
358
  const recipient = await trx('automated_email_recipients')
354
359
  .select('id', 'clicked_at')
355
360
  .where({
356
361
  automation_action_revision_id: automationActionRevisionId,
362
+ automation_run_step_id: automationRunStepId,
357
363
  member_id: memberId,
358
364
  track_clicks: true
359
365
  })
360
- .where('created_at', '<=', toDatabaseDate(clickedAt))
361
- .orderBy('created_at', 'desc')
362
- .orderBy('id', 'desc')
363
366
  .forUpdate()
364
367
  .first();
365
368
 
@@ -394,6 +397,29 @@ export function createDatabaseAutomationsRepository({
394
397
  };
395
398
  }
396
399
 
400
+ /**
401
+ * Lock revisions before recipients because inserting a recipient takes a shared
402
+ * foreign-key lock on its revision. Updating that revision later can deadlock
403
+ * with another transaction that has already locked the revision and is waiting
404
+ * for the recipient. Keep multi-revision lock acquisition deterministic.
405
+ */
406
+ async function lockActionRevisions(
407
+ trx: Knex.Transaction,
408
+ revisionIds: Iterable<string>
409
+ ): Promise<string[]> {
410
+ const sortedRevisionIds = [...new Set(revisionIds)]
411
+ .sort((left, right) => left.localeCompare(right));
412
+
413
+ if (sortedRevisionIds.length > 0) {
414
+ await trx('automation_action_revisions')
415
+ .select('id')
416
+ .whereIn('id', sortedRevisionIds)
417
+ .forUpdate();
418
+ }
419
+
420
+ return sortedRevisionIds;
421
+ }
422
+
397
423
  /**
398
424
  * Which of these recipients have yet to open, and so should count towards their
399
425
  * revision's open count. Locks them for the transaction, so a worker racing on
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getSchedulerIdempotencyKey = getSchedulerIdempotencyKey;
7
+ const node_crypto_1 = __importDefault(require("node:crypto"));
8
+ function getSchedulerIdempotencyKey(date, url) {
9
+ const hash = node_crypto_1.default.createHash('sha256');
10
+ hash.update(date.toISOString());
11
+ hash.update(url.href);
12
+ return `ghost-automations-${hash.digest('hex')}`;
13
+ }
@@ -0,0 +1,8 @@
1
+ import crypto from 'node:crypto';
2
+
3
+ export function getSchedulerIdempotencyKey(date: Readonly<Date>, url: Readonly<URL>): string {
4
+ const hash = crypto.createHash('sha256');
5
+ hash.update(date.toISOString());
6
+ hash.update(url.href);
7
+ return `ghost-automations-${hash.digest('hex')}`;
8
+ }
@@ -129,7 +129,8 @@ const processStep = async ({ automationsApi, memberWelcomeEmailService, schedule
129
129
  memberStatus,
130
130
  trackOpens,
131
131
  trackClicks,
132
- automationActionRevisionId: step.automation_action_revision_id
132
+ automationActionRevisionId: step.automation_action_revision_id,
133
+ automationRunStepId: step.id
133
134
  });
134
135
  const mailgunMessageId = (0, mailgun_message_id_1.getMailgunMessageId)(sendResult);
135
136
  // Only Mailgun sends can produce open events for automation emails
@@ -137,6 +138,7 @@ const processStep = async ({ automationsApi, memberWelcomeEmailService, schedule
137
138
  try {
138
139
  await automationsApi.recordEmailSent({
139
140
  automationActionRevisionId: step.automation_action_revision_id,
141
+ automationRunStepId: step.id,
140
142
  ...(mailgunMessageId ? { mailgunMessageId } : {}),
141
143
  memberEmail: member.get('email'),
142
144
  memberId: step.member_id,
@@ -28,6 +28,7 @@ type MemberWelcomeEmailService = {
28
28
  trackOpens: boolean;
29
29
  trackClicks: boolean;
30
30
  automationActionRevisionId: string;
31
+ automationRunStepId: string;
31
32
  }) => Promise<unknown>;
32
33
  };
33
34
  };
@@ -202,7 +203,8 @@ const processStep = async ({
202
203
  memberStatus,
203
204
  trackOpens,
204
205
  trackClicks,
205
- automationActionRevisionId: step.automation_action_revision_id
206
+ automationActionRevisionId: step.automation_action_revision_id,
207
+ automationRunStepId: step.id
206
208
  });
207
209
  const mailgunMessageId = getMailgunMessageId(sendResult);
208
210
  // Only Mailgun sends can produce open events for automation emails
@@ -210,6 +212,7 @@ const processStep = async ({
210
212
  try {
211
213
  await automationsApi.recordEmailSent({
212
214
  automationActionRevisionId: step.automation_action_revision_id,
215
+ automationRunStepId: step.id,
213
216
  ...(mailgunMessageId ? {mailgunMessageId} : {}),
214
217
  memberEmail: member.get('email'),
215
218
  memberId: step.member_id,
@@ -41,6 +41,7 @@ const strict_1 = __importDefault(require("node:assert/strict"));
41
41
  const one_at_a_time_1 = require("../../../shared/one-at-a-time");
42
42
  const poll_1 = require("./poll");
43
43
  const automationsApi = __importStar(require("./automations-api"));
44
+ const get_scheduler_idempotency_key_1 = require("./get-scheduler-idempotency-key");
44
45
  const promises_1 = require("node:timers/promises");
45
46
  const soonest_timer_1 = require("../../lib/soonest-timer");
46
47
  const scheduler_poll_time_1 = require("./scheduler-poll-time");
@@ -101,20 +102,23 @@ class AutomationsService {
101
102
  schedulerAdapter.schedule({
102
103
  time: schedulerPollTime.getTime(),
103
104
  url: url.toString(),
104
- extra: { httpMethod: 'PUT' }
105
+ extra: {
106
+ httpMethod: 'PUT',
107
+ idempotencyKey: (0, get_scheduler_idempotency_key_1.getSchedulerIdempotencyKey)(date, url)
108
+ }
105
109
  });
106
110
  }
107
111
  catch (err) {
108
112
  logging.error({ event: { name: 'automations.enqueue-poll.error' }, err, at: date.toISOString() }, 'Failed to enqueue automations poll');
109
113
  }
110
114
  };
111
- domainEvents.subscribe(StartAutomationsPollEvent, (0, one_at_a_time_1.oneAtATime)(async () => (0, poll_1.poll)({
115
+ domainEvents.subscribe(StartAutomationsPollEvent, (0, one_at_a_time_1.oneAtATime)(() => (0, poll_1.poll)({
112
116
  automationsApi,
113
117
  memberWelcomeEmailService,
114
118
  scheduleAutomationEmailAnalyticsJob,
115
119
  enqueueAnotherPollAt: enqueuePollAt
116
120
  })));
117
- domainEvents.subscribe(StartAutomationsPollEvent, (0, one_at_a_time_1.oneAtATime)(async () => welcomeEmailAutomationPoll({
121
+ domainEvents.subscribe(StartAutomationsPollEvent, (0, one_at_a_time_1.oneAtATime)(() => welcomeEmailAutomationPoll({
118
122
  memberWelcomeEmailService,
119
123
  enqueueAnotherPollAt: enqueuePollAt
120
124
  })));
@@ -6,6 +6,7 @@ import type DomainEvents from '@tryghost/domain-events';
6
6
  import {oneAtATime} from '../../../shared/one-at-a-time';
7
7
  import {poll} from './poll';
8
8
  import * as automationsApi from './automations-api';
9
+ import {getSchedulerIdempotencyKey} from './get-scheduler-idempotency-key';
9
10
  import {setImmediate as flushEventLoop} from 'node:timers/promises';
10
11
  import {SoonestTimer} from '../../lib/soonest-timer';
11
12
  import {getSchedulerPollTime} from './scheduler-poll-time';
@@ -85,21 +86,24 @@ export class AutomationsService {
85
86
  schedulerAdapter.schedule({
86
87
  time: schedulerPollTime.getTime(),
87
88
  url: url.toString(),
88
- extra: {httpMethod: 'PUT'}
89
+ extra: {
90
+ httpMethod: 'PUT',
91
+ idempotencyKey: getSchedulerIdempotencyKey(date, url)
92
+ }
89
93
  });
90
94
  } catch (err) {
91
95
  logging.error({event: {name: 'automations.enqueue-poll.error'}, err, at: date.toISOString()}, 'Failed to enqueue automations poll');
92
96
  }
93
97
  };
94
98
 
95
- domainEvents.subscribe(StartAutomationsPollEvent, oneAtATime(async () => poll({
99
+ domainEvents.subscribe(StartAutomationsPollEvent, oneAtATime(() => poll({
96
100
  automationsApi,
97
101
  memberWelcomeEmailService,
98
102
  scheduleAutomationEmailAnalyticsJob,
99
103
  enqueueAnotherPollAt: enqueuePollAt
100
104
  })));
101
105
 
102
- domainEvents.subscribe(StartAutomationsPollEvent, oneAtATime(async () => welcomeEmailAutomationPoll({
106
+ domainEvents.subscribe(StartAutomationsPollEvent, oneAtATime(() => welcomeEmailAutomationPoll({
103
107
  memberWelcomeEmailService,
104
108
  enqueueAnotherPollAt: enqueuePollAt
105
109
  })));
@@ -29,7 +29,7 @@ class CommentsServiceEmails {
29
29
  // Comments live on pages as well as posts; the URL service routes by
30
30
  // the passed type, so a page typed 'posts' resolves to /404/.
31
31
  const routerType = postData.type === 'page' ? 'pages' : 'posts';
32
- const baseUrl = this.urlService.facade.getUrlForResource({...postData, type: routerType}, {absolute: true});
32
+ const baseUrl = this.urlService.getUrlForResource({...postData, type: routerType}, {absolute: true});
33
33
  return `${baseUrl}#ghost-comments-${commentId}`;
34
34
  }
35
35
 
@@ -473,7 +473,7 @@ class CommentsService {
473
473
  * @param {AdminBrowseAllOptions} options
474
474
  */
475
475
  async getAdminAllComments({includeNested, filter, mongoTransformer, reportCount, order, page, limit}) {
476
- const postUrlRelations = this.urlService.facade.getRequiredRelations().map(relation => `post.${relation}`);
476
+ const postUrlRelations = this.urlService.getRequiredRelations().map(relation => `post.${relation}`);
477
477
  const withRelated = ['member', 'post', ...postUrlRelations, 'count.replies', 'count.direct_replies', 'count.likes', 'count.dislikes', 'count.net_score', 'count.reports', 'in_reply_to', 'parent'];
478
478
 
479
479
  return await this.models.Comment.findPage({
@@ -4,7 +4,7 @@ const config = require('../../../shared/config');
4
4
  const domainEvents = require('@tryghost/domain-events');
5
5
  /** @import {PrometheusClient} from '@tryghost/prometheus-metrics' */
6
6
  /** @import {BatchEventProcessor} from './batch-event-processor' */
7
- /** @import {JobNames, CursorSeed} from './email-analytics-service' */
7
+ /** @import {JobNames, CursorSeed, EmailAnalyticsFetchResult} from './email-analytics-service' */
8
8
 
9
9
  class EmailAnalyticsServiceWrapper {
10
10
  /** @type {string} */ #logName;
@@ -74,7 +74,7 @@ class EmailAnalyticsServiceWrapper {
74
74
  /**
75
75
  * Log comprehensive job completion with timing metrics
76
76
  * @param {string} jobType - Type of job (e.g., 'latest-opened', 'latest', 'missing', 'scheduled')
77
- * @param {object} fetchResult - The fetch result from EmailAnalyticsService
77
+ * @param {EmailAnalyticsFetchResult} fetchResult - The fetch result from EmailAnalyticsService
78
78
  * @param {number} totalDurationMs - Total duration in milliseconds
79
79
  */
80
80
  _logJobCompletion(jobType, fetchResult, totalDurationMs) {
@@ -158,6 +158,11 @@ class EmailAnalyticsServiceWrapper {
158
158
  return fetchResult.eventCount;
159
159
  }
160
160
 
161
+ /**
162
+ * @param {object} options
163
+ * @param {number} options.maxEvents
164
+ * @returns {Promise<number>} The number of scheduled events fetched
165
+ */
161
166
  async fetchScheduled({maxEvents}) {
162
167
  if (maxEvents < 300) {
163
168
  return 0;
@@ -227,6 +232,10 @@ class EmailAnalyticsServiceWrapper {
227
232
  this.#fetching = false;
228
233
  }
229
234
 
235
+ /**
236
+ * @param {string} reason
237
+ * @returns {void}
238
+ */
230
239
  _restartFetch(reason) {
231
240
  this.#fetching = false;
232
241
  logging.info(`${this.#logPrefix} Restarting fetch due to ${reason}`);
@@ -372,7 +372,7 @@ module.exports = class EmailAnalyticsService {
372
372
  * @param {EmailAnalyticsEvent[]} [options.eventTypes] - Array of event types to fetch. If not provided, Mailgun will return all event types.
373
373
  * @returns {Promise<EmailAnalyticsFetchResult>} Fetch results with timing metrics
374
374
  */
375
- async #fetchEvents(fetchData, {begin, end, maxEvents = Infinity, eventTypes = null}) {
375
+ async #fetchEvents(fetchData, {begin, end, maxEvents = Infinity, eventTypes}) {
376
376
  // Start where we left of, or the last stored event in the database, or start 30 minutes ago if we have nothing available
377
377
  // Store that we started fetching
378
378
  fetchData.running = true;
@@ -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
  }
@@ -0,0 +1,9 @@
1
+ # Gift Links
2
+
3
+ Gift Links covers shareable access to individual protected posts and pages without creating a membership.
4
+
5
+ ## Language
6
+
7
+ **Gift Link**:
8
+ A revocable link that grants its holder access to one protected post or page. It does not create or redeem a gift subscription.
9
+ _Avoid_: Redemption link, gift-subscription link