ghost 6.58.0 → 6.59.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 (453) hide show
  1. package/components/tryghost-adapter-base-scheduling-0.2.2.tgz +0 -0
  2. package/components/tryghost-adapter-base-sso-0.1.2.tgz +0 -0
  3. package/components/tryghost-i18n-0.0.0.tgz +0 -0
  4. package/components/tryghost-kg-default-nodes-2.2.0.tgz +0 -0
  5. package/components/tryghost-kg-html-to-lexical-1.4.0.tgz +0 -0
  6. package/components/tryghost-kg-lexical-html-renderer-1.5.0.tgz +0 -0
  7. package/content/themes/casper/.github/workflows/test.yml +11 -1
  8. package/content/themes/casper/AGENTS.md +25 -0
  9. package/content/themes/casper/assets/built/screen.css +1 -1
  10. package/content/themes/casper/assets/css/screen.css +15 -1
  11. package/content/themes/casper/gulpfile.js +3 -0
  12. package/content/themes/casper/package.json +8 -8
  13. package/content/themes/source/.gitattributes +5 -0
  14. package/content/themes/source/.github/workflows/test.yml +39 -0
  15. package/content/themes/source/AGENTS.md +25 -0
  16. package/content/themes/source/README.md +33 -4
  17. package/content/themes/source/assets/built/screen.css +1 -1
  18. package/content/themes/source/assets/css/screen.css +15 -0
  19. package/content/themes/source/gulpfile.js +6 -3
  20. package/content/themes/source/package.json +13 -10
  21. package/content/themes/source/pnpm-workspace.yaml +3 -0
  22. package/core/boot.js +11 -4
  23. package/core/built/admin/assets/Combination-BCm9FL4o.js +41 -0
  24. package/core/built/admin/assets/about-BdSCpjXG.js +1 -0
  25. package/core/built/admin/assets/account-migration-BpKAqo8k.js +1 -0
  26. package/core/built/admin/assets/activitypub/{account-migration-BpDeIF3G.js → account-migration-DVnYS1XG.js} +8 -8
  27. package/core/built/admin/assets/activitypub/activitypub.js +2 -2
  28. package/core/built/admin/assets/activitypub/{bluesky-sharing-aPMtrS3L.js → bluesky-sharing-DLVeE9ii.js} +31 -31
  29. package/core/built/admin/assets/activitypub/{content-formatters-BORfuA9G.js → content-formatters-DCsVic-t.js} +2 -2
  30. package/core/built/admin/assets/activitypub/{dist-ByFmDSNP2.js → dist-ByFmDSNP.js} +1 -1
  31. package/core/built/admin/assets/activitypub/{domain-CPdiB27_.js → domain-rrvDLfnK.js} +8 -8
  32. package/core/built/admin/assets/activitypub/{edit-profile-BY2PawIe.js → edit-profile-VT-piVxq.js} +23 -23
  33. package/core/built/admin/assets/activitypub/{explore-Sp9bgAwq.js → explore-DZg3m48P.js} +14 -14
  34. package/core/built/admin/assets/activitypub/{feed-BjtF4ohL.js → feed-BJqQvDMI.js} +41 -41
  35. package/core/built/admin/assets/activitypub/{field-BDOYVTg6.js → field-BezS4-1J.js} +2 -2
  36. package/core/built/admin/assets/activitypub/inbox-Uf0GVrI_.js +1284 -0
  37. package/core/built/admin/assets/activitypub/{label-1jhN-qhD.js → label-CZ6if4zK.js} +2 -2
  38. package/core/built/admin/assets/activitypub/layout-DNuYT4Gu.js +1 -0
  39. package/core/built/admin/assets/activitypub/moderation-CP6mDVky.js +340 -0
  40. package/core/built/admin/assets/activitypub/{note-nr_m_5yb.js → note-BlFkye2g.js} +16 -16
  41. package/core/built/admin/assets/activitypub/{notifications-BdcyivbT.js → notifications-GGTrGEdu.js} +163 -126
  42. package/core/built/admin/assets/activitypub/{preferences-CNqy1x9C.js → preferences-CjZXrZqr.js} +1550 -1551
  43. package/core/built/admin/assets/activitypub/{profile-DlY4rhbO.js → profile-CcXYxydm.js} +248 -248
  44. package/core/built/admin/assets/activitypub/{routes-CEHdEZzS.js → routes-nHCKlZl-.js} +1971 -1747
  45. package/core/built/admin/assets/activitypub/settings-pDzybcb3.js +315 -0
  46. package/core/built/admin/assets/activitypub/{site-CtBUCPBz.js → site-BZF0F8LN.js} +101 -101
  47. package/core/built/admin/assets/activitypub/{step-1-D9jGWyAy.js → step-1-C-Ns1Tfg.js} +3 -3
  48. package/core/built/admin/assets/activitypub/{step-3-KpUqpGWi.js → step-3-rDMNGmLg.js} +8 -8
  49. package/core/built/admin/assets/activitypub/{tabs-DvlsxTCI.js → tabs-CDjh3_Jp.js} +198 -198
  50. package/core/built/admin/assets/activitypub/{topic-filter-CIN1E2jg.js → topic-filter-CmTc8hpN.js} +2 -2
  51. package/core/built/admin/assets/activitypub/{use-activity-pub-queries-CATFd6YV.js → use-activity-pub-queries-DUDEdSdR.js} +286 -243
  52. package/core/built/admin/assets/activitypub/{use-reply-chain-data-DJ0vhA8V.js → use-reply-chain-data-TunGtmJl.js} +14 -14
  53. package/core/built/admin/assets/add-integration-modal-CXRQi1x0.js +1 -0
  54. package/core/built/admin/assets/add-newsletter-modal-t447KRYz.js +1 -0
  55. package/core/built/admin/assets/add-recommendation-modal-D882MdnR.js +1 -0
  56. package/core/built/admin/assets/{analytics-BC2oR8ay.js → analytics-Dj-WlzPf.js} +1 -1
  57. package/core/built/admin/assets/announcement-bar-modal-CwHKKw-a.js +1 -0
  58. package/core/built/admin/assets/api-keys-Boy5pLbD.js +1 -0
  59. package/core/built/admin/assets/audience-Cc40y7zQ.js +1 -0
  60. package/core/built/admin/assets/automations-DNLK5Udr.js +1 -0
  61. package/core/built/admin/assets/bluesky-sharing-Cm4aFaOl.js +1 -0
  62. package/core/built/admin/assets/breadcrumb-CcFr_ltt.js +1 -0
  63. package/core/built/admin/assets/button-BshWPHvn.js +1 -0
  64. package/core/built/admin/assets/chart-helpers-BsbzoWhK.js +1 -0
  65. package/core/built/admin/assets/{checkbox-LmClhqcS.js → checkbox-1G6KfK2q.js} +1 -1
  66. package/core/built/admin/assets/chunk-KS7C4IRE-DCsL4i4D.js +4 -0
  67. package/core/built/admin/assets/{chunk.408.1a258af360eb8d905508.js → chunk.301.5673a0caf7c49bb00212.js} +3 -3
  68. package/core/built/admin/assets/{chunk.524.4c693c3a00ac91df34fa.js → chunk.524.86db0d945468c852374b.js} +4 -4
  69. package/core/built/admin/assets/{chunk.582.20b600e329e58d12d7c2.js → chunk.582.c2f6216d3c4af303e236.js} +6 -6
  70. package/core/built/admin/assets/{code-editor-view-DqTOUnbM.js → code-editor-view-BVjtWyst.js} +1 -1
  71. package/core/built/admin/assets/{code-editor-view-bA0lfxUL.js → code-editor-view-y_zVfmCa.js} +1 -1
  72. package/core/built/admin/assets/color-picker-XEMPgNJE.js +3 -0
  73. package/core/built/admin/assets/{color-picker-field-Cv9GOJQZ.js → color-picker-field-BhplXcy5.js} +1 -1
  74. package/core/built/admin/assets/combobox-CRMJrvcQ.js +1 -0
  75. package/core/built/admin/assets/command-151T2eRx.js +1 -0
  76. package/core/built/admin/assets/comments-Bjbl9toF.js +1 -0
  77. package/core/built/admin/assets/confirmation-context-QmFxSjWy.js +1 -0
  78. package/core/built/admin/assets/{content-api-modal-BOFWRUkC.js → content-api-modal-CpeABXne.js} +1 -1
  79. package/core/built/admin/assets/{copy-field-8biQvNMD.js → copy-field-CglgRZyG.js} +1 -1
  80. package/core/built/admin/assets/custom-field-type-option-DSyJw4T4.js +1 -0
  81. package/core/built/admin/assets/custom-integration-modal-DWB9bRVp.js +1 -0
  82. package/core/built/admin/assets/date-range-select-kaKtab5O.js +1 -0
  83. package/core/built/admin/assets/design-and-theme-modal-86s2AaZ-.js +23 -0
  84. package/core/built/admin/assets/dirty-confirm-dialog-CTFdXXy7.js +1 -0
  85. package/core/built/admin/assets/disabled-sources-indicator-BFyCxUOx.js +1 -0
  86. package/core/built/admin/assets/dist-C1-Oy3Db.js +1 -0
  87. package/core/built/admin/assets/dist-CJJqYPep.js +1 -0
  88. package/core/built/admin/assets/dist-CNlMj7ML.js +1 -0
  89. package/core/built/admin/assets/dist-Kll2twSG.js +1 -0
  90. package/core/built/admin/assets/domain-BOoMvBG3.js +1 -0
  91. package/core/built/admin/assets/{dropzone-B21Entqz.js → dropzone-B6KWzsrb.js} +2 -2
  92. package/core/built/admin/assets/edit-profile-BagrYOrN.js +1 -0
  93. package/core/built/admin/assets/editor-AaJjqbpk.js +8 -0
  94. package/core/built/admin/assets/embed-signup-form-modal-B8L95KwR.js +8 -0
  95. package/core/built/admin/assets/explore-xbykZTlO.js +1 -0
  96. package/core/built/admin/assets/feed-BAHSipWv.js +1 -0
  97. package/core/built/admin/assets/{field-BQqLTgHM.js → field-kJYZ2T3C.js} +1 -1
  98. package/core/built/admin/assets/{filter-sources-FgqoUIL7.js → filter-sources-QeqFGNRB.js} +1 -1
  99. package/core/built/admin/assets/{filters-D0uIMD8n.js → filters-By5KQeV6.js} +1 -1
  100. package/core/built/admin/assets/filters-DtYpAkDd.js +1 -0
  101. package/core/built/admin/assets/first-promoter-modal-8A02oyjA.js +1 -0
  102. package/core/built/admin/assets/{ghost-ba408063a5922b458b4a8f216cf23251.js → ghost-f110822e67868c9fdf9a6c23754b2559.js} +9 -8
  103. package/core/built/admin/assets/gift-link-modal-DbYCyuhF.js +1 -0
  104. package/core/built/admin/assets/global-data-context-gAMDH9u1.js +1 -0
  105. package/core/built/admin/assets/growth-BSOxJGey.js +3 -0
  106. package/core/built/admin/assets/growth-BZTjURq5.js +1 -0
  107. package/core/built/admin/assets/history-modal-CCgWtlZE.js +1 -0
  108. package/core/built/admin/assets/html-field-BQC7XyLA.js +1 -0
  109. package/core/built/admin/assets/iframe-buffering-DnOlWTD4.js +1 -0
  110. package/core/built/admin/assets/{images-3cQgwfTD.js → images-D5UKr2SG.js} +1 -1
  111. package/core/built/admin/assets/{inbox-Dz_JblyJ.js → inbox-DvN15_YH.js} +45 -39
  112. package/core/built/admin/assets/index-BJz9Zlsc.js +67 -0
  113. package/core/built/admin/assets/index-Dg8cdR8E.css +1 -0
  114. package/core/built/admin/assets/{input-group-DpogNUIt.js → input-group-BPAxETpQ.js} +1 -1
  115. package/core/built/admin/assets/invite-user-modal-jcUaaNkW.js +1 -0
  116. package/core/built/admin/assets/{koenig-lexical-2KuqRwZI.js → koenig-lexical-CfXTYeli.js} +1 -1
  117. package/core/built/admin/assets/koenig-loader-DktFM3Uh.js +1 -0
  118. package/core/built/admin/assets/{kpi-card-Ck3tT2Wg.js → kpi-card-fUHZlzBX.js} +1 -1
  119. package/core/built/admin/assets/{kpi-card-DDIxptaS.js → kpi-card-o0urVtNL.js} +1 -1
  120. package/core/built/admin/assets/kpi-helpers-2FTF4a3-.js +1 -0
  121. package/core/built/admin/assets/kpi-tabs-B8-_k2dQ.js +1 -0
  122. package/core/built/admin/assets/{label-DodDsMzn.js → label-CiRS3i_f.js} +1 -1
  123. package/core/built/admin/assets/label-picker-ao7tuPaK.js +1 -0
  124. package/core/built/admin/assets/layout-DC6QGDaw.js +1 -0
  125. package/core/built/admin/assets/member-custom-fields-BW756-EV.js +1 -0
  126. package/core/built/admin/assets/member-detail-CGGgUTSl.js +1 -0
  127. package/core/built/admin/assets/members-ZcPthwG0.js +11 -0
  128. package/core/built/admin/assets/moderation-24bElWs3.js +1 -0
  129. package/core/built/admin/assets/multi-select-combobox-CZPrgz-Z.js +1 -0
  130. package/core/built/admin/assets/navigation-modal-BiTP4U_J.js +1 -0
  131. package/core/built/admin/assets/newsletter-B3Ihwizp.js +1 -0
  132. package/core/built/admin/assets/newsletter-detail-modal-CYsu4qhl.js +1 -0
  133. package/core/built/admin/assets/newsletter-emails-CDPR4-SP.js +1 -0
  134. package/core/built/admin/assets/newsletters-bJiYLJWl.js +1 -0
  135. package/core/built/admin/assets/note-7f6urVVA.js +1 -0
  136. package/core/built/admin/assets/notifications-rosQCyXH.js +1 -0
  137. package/core/built/admin/assets/offers-container-modal-J2j6NpDL.js +1 -0
  138. package/core/built/admin/assets/onboarding-route-DTjcThKl.js +1 -0
  139. package/core/built/admin/assets/overview-BxvWzGaf.js +1 -0
  140. package/core/built/admin/assets/overview-D6JG0hkZ.js +1 -0
  141. package/core/built/admin/assets/{page-header-VLPJfj_d.js → page-header-l7vXmLIw.js} +1 -1
  142. package/core/built/admin/assets/pintura-modal-BGHmudVB.js +1 -0
  143. package/core/built/admin/assets/portal-frame-DVc4VHX_.js +1 -0
  144. package/core/built/admin/assets/portal-modal-BCOcdiRi.js +1 -0
  145. package/core/built/admin/assets/{post-analytics-Gv81eN_g.js → post-analytics-CTSZMK6I.js} +1 -1
  146. package/core/built/admin/assets/post-analytics-header-Dt4RQGq0.js +1 -0
  147. package/core/built/admin/assets/{post-analytics-provider-O2YjZMhN.js → post-analytics-provider-C4ydiboO.js} +1 -1
  148. package/core/built/admin/assets/post-share-modal-DjH_a_1L.js +1 -0
  149. package/core/built/admin/assets/preferences-BLo6VJUS.js +5 -0
  150. package/core/built/admin/assets/preview-modal-BOgjgsJL.js +1 -0
  151. package/core/built/admin/assets/profile-DG6iCdRH.js +1 -0
  152. package/core/built/admin/assets/radio-group-ChHD4EQJ.js +1 -0
  153. package/core/built/admin/assets/router-provider-Bexg1t-V.js +1 -0
  154. package/core/built/admin/assets/select-ChxknEcT.js +1 -0
  155. package/core/built/admin/assets/settings-B0VDAhgV.js +1 -0
  156. package/core/built/admin/assets/settings-Dwo3CeAD.js +4 -0
  157. package/core/built/admin/assets/settings-breadcrumbs-sLDNedBW.js +1 -0
  158. package/core/built/admin/assets/share-modal-BDb2nacg.js +1 -0
  159. package/core/built/admin/assets/skeleton-D7hwjdry.js +1 -0
  160. package/core/built/admin/assets/slack-modal-CwNAD69x.js +1 -0
  161. package/core/built/admin/assets/sort-button-DhqpxkrO.js +1 -0
  162. package/core/built/admin/assets/source-icon-BPsD5ees.js +1 -0
  163. package/core/built/admin/assets/stats-WdDJ8c_1.js +1 -0
  164. package/core/built/admin/assets/stats-view-J3PI2eT_.js +1 -0
  165. package/core/built/admin/assets/step-1-2q1SjerQ.js +1 -0
  166. package/core/built/admin/assets/step-2-D5mJB3Uu.js +1 -0
  167. package/core/built/admin/assets/step-3-BpAuYTGZ.js +2 -0
  168. package/core/built/admin/assets/{stripe-button-Bh4p6Wud.js → stripe-button-DDoUKN5a.js} +1 -1
  169. package/core/built/admin/assets/stripe-connect-modal-pMdF3t4S.js +1 -0
  170. package/core/built/admin/assets/switch-B_1yU3Aw.js +1 -0
  171. package/core/built/admin/assets/{table-DghJxZMH.js → table-CrqQtkR2.js} +1 -1
  172. package/core/built/admin/assets/tabs-BhzVdh5p.js +1 -0
  173. package/core/built/admin/assets/tag-detail-CwJhI4qg.js +2 -0
  174. package/core/built/admin/assets/tags-CvJ0PgrB.js +1 -0
  175. package/core/built/admin/assets/testimonials-modal-CV82IAnI.js +1 -0
  176. package/core/built/admin/assets/tier-detail-modal-pLzCOfeg.js +1 -0
  177. package/core/built/admin/assets/toggle-group-BW_T4yn6.js +1 -0
  178. package/core/built/admin/assets/topic-filter-DkXjDejV.js +1 -0
  179. package/core/built/admin/assets/transistor-modal-TMRvHZ6B.js +1 -0
  180. package/core/built/admin/assets/unsplash-modal-CV92RdPw.js +1 -0
  181. package/core/built/admin/assets/{use-analytics-data-IxJcSjc9.js → use-analytics-data-BM_uV9SU.js} +1 -1
  182. package/core/built/admin/assets/use-check-theme-limit-error-DkBnXMSY.js +1 -0
  183. package/core/built/admin/assets/use-confirm-unload-SzlkOIpm.js +1 -0
  184. package/core/built/admin/assets/use-feature-flag-cahOlMva.js +1 -0
  185. package/core/built/admin/assets/use-filter-params-BX5qf0Cg.js +1 -0
  186. package/core/built/admin/assets/{use-growth-stats-1NW-8eEZ.js → use-growth-stats-D0LlWBe6.js} +1 -1
  187. package/core/built/admin/assets/{use-hash-link-navigation-guard-B9j6HIrV.js → use-hash-link-navigation-guard-B_Y3FhYH.js} +1 -1
  188. package/core/built/admin/assets/use-limiter-D3DHSoAj.js +2 -0
  189. package/core/built/admin/assets/{use-pintura-config-BjFChXV1.js → use-pintura-config-D63Xu0SG.js} +1 -1
  190. package/core/built/admin/assets/use-pintura-editor-Z51aFnC8.js +1 -0
  191. package/core/built/admin/assets/{use-post-referrers-Dp1BnTAK.js → use-post-referrers-Cvymr3gt.js} +1 -1
  192. package/core/built/admin/assets/use-query-params-BfzuqbcD.js +1 -0
  193. package/core/built/admin/assets/use-reply-chain-data-Dc18Ujqo.js +1 -0
  194. package/core/built/admin/assets/use-setting-group-DYRIzqC0.js +1 -0
  195. package/core/built/admin/assets/use-settings-navigation-aDSLtmEl.js +1 -0
  196. package/core/built/admin/assets/{use-simple-pagination-DsEGSJhe.js → use-simple-pagination-B2TdNX67.js} +1 -1
  197. package/core/built/admin/assets/{use-tinybird-query-Ci_DiPkX.js → use-tinybird-query-BvQIVuRc.js} +1 -1
  198. package/core/built/admin/assets/{use-tinybird-token-CsUwc722.js → use-tinybird-token-COQTPIwL.js} +1 -1
  199. package/core/built/admin/assets/use-upgrade-route-BZf1g59m.js +1 -0
  200. package/core/built/admin/assets/{use-url-input-BEZJpVgN.js → use-url-input-PcqZadXa.js} +1 -1
  201. package/core/built/admin/assets/user-detail-modal-DeO3SAZq.js +1 -0
  202. package/core/built/admin/assets/{virtual-list-D5ugXyZY.js → virtual-list-D3MfPNOh.js} +1 -1
  203. package/core/built/admin/assets/web-CDdyG7z1.js +2 -0
  204. package/core/built/admin/assets/web-CwQ8_QUc.js +1 -0
  205. package/core/built/admin/assets/zapier-modal-a7t75iNI.js +1 -0
  206. package/core/built/admin/index.html +15 -12
  207. package/core/frontend/apps/private-blogging/lib/helpers/input_password.js +2 -1
  208. package/core/frontend/helpers/body_class.js +2 -1
  209. package/core/frontend/helpers/cancel_link.js +2 -1
  210. package/core/frontend/helpers/color_to_rgba.js +2 -1
  211. package/core/frontend/helpers/content_api_key.js +2 -1
  212. package/core/frontend/helpers/content_api_url.js +2 -1
  213. package/core/frontend/helpers/contrast_text_color.js +2 -1
  214. package/core/frontend/helpers/facebook_url.js +2 -1
  215. package/core/frontend/helpers/ghost_foot.js +2 -1
  216. package/core/frontend/helpers/ghost_head.js +2 -1
  217. package/core/frontend/helpers/has.js +4 -2
  218. package/core/frontend/helpers/link_class.js +2 -1
  219. package/core/frontend/helpers/meta_description.js +2 -1
  220. package/core/frontend/helpers/meta_title.js +2 -1
  221. package/core/frontend/helpers/navigation.js +113 -22
  222. package/core/frontend/helpers/page_url.js +2 -1
  223. package/core/frontend/helpers/post_class.js +2 -1
  224. package/core/frontend/helpers/reading_time.js +2 -1
  225. package/core/frontend/helpers/social_accounts.js +2 -1
  226. package/core/frontend/helpers/social_url.js +2 -1
  227. package/core/frontend/helpers/total_members.js +2 -1
  228. package/core/frontend/helpers/total_paid_members.js +2 -1
  229. package/core/frontend/helpers/tpl/navigation.hbs +1 -1
  230. package/core/frontend/helpers/twitter_url.js +2 -1
  231. package/core/frontend/public/cards.manifest.json +1 -1
  232. package/core/server/api/endpoints/posts.js +7 -4
  233. package/core/server/api/endpoints/utils/permissions.js +2 -1
  234. package/core/server/api/endpoints/utils/validators/input/settings.js +83 -1
  235. package/core/server/data/exporter/table-lists.js +1 -0
  236. package/core/server/data/importer/handlers/json.js +2 -1
  237. package/core/server/data/migrations/versions/5.19/2022-09-02-20-52-backfill-new-product-columns.js +2 -1
  238. package/core/server/data/migrations/versions/5.21/2022-10-26-04-50-member-subscription-created-batch-id.js +4 -2
  239. package/core/server/data/migrations/versions/5.22/2022-10-31-12-03-backfill-new-product-columns.js +2 -1
  240. package/core/server/data/migrations/versions/6.58/2026-08-11-21-22-48-rename-email-batches-provider-id.js +1 -1
  241. package/core/server/data/migrations/versions/6.59/2026-08-18-17-06-46-add-gift-delivery-fields-to-gifts.js +26 -0
  242. package/core/server/data/migrations/versions/6.59/2026-08-18-17-07-01-add-gift-deliveries.js +20 -0
  243. package/core/server/data/schema/commands.js +48 -24
  244. package/core/server/data/schema/schema.js +28 -7
  245. package/core/server/data/seeders/importers/automation-action-revisions-importer.js +3 -36
  246. package/core/server/data/seeders/importers/automation-actions-importer.js +5 -41
  247. package/core/server/data/seeders/importers/automation-run-steps-importer.js +3 -36
  248. package/core/server/data/seeders/importers/automation-runs-importer.js +7 -42
  249. package/core/server/data/seeders/importers/automations-importer.js +3 -36
  250. package/core/server/data/seeders/importers/comment-reports-importer.js +5 -5
  251. package/core/server/data/seeders/importers/comments-importer.js +5 -5
  252. package/core/server/data/seeders/importers/email-batches-importer.js +5 -4
  253. package/core/server/data/seeders/importers/email-recipients-importer.js +12 -20
  254. package/core/server/data/seeders/importers/emails-importer.js +6 -6
  255. package/core/server/data/seeders/importers/labels-importer.js +3 -3
  256. package/core/server/data/seeders/importers/members-click-events-importer.js +5 -5
  257. package/core/server/data/seeders/importers/members-created-events-importer.js +13 -12
  258. package/core/server/data/seeders/importers/members-feedback-importer.js +7 -7
  259. package/core/server/data/seeders/importers/members-importer.js +4 -4
  260. package/core/server/data/seeders/importers/members-login-events-importer.js +3 -3
  261. package/core/server/data/seeders/importers/members-products-importer.js +2 -2
  262. package/core/server/data/seeders/importers/members-status-events-importer.js +4 -3
  263. package/core/server/data/seeders/importers/members-stripe-customers-importer.js +2 -2
  264. package/core/server/data/seeders/importers/members-stripe-customers-subscriptions-importer.js +7 -7
  265. package/core/server/data/seeders/importers/members-subscribe-events-importer.js +3 -3
  266. package/core/server/data/seeders/importers/members-subscription-created-events-importer.js +3 -3
  267. package/core/server/data/seeders/importers/offer-redemptions-importer.js +7 -7
  268. package/core/server/data/seeders/importers/offers-importer.js +3 -3
  269. package/core/server/data/seeders/importers/posts-importer.js +4 -4
  270. package/core/server/data/seeders/importers/table-importer.js +2 -35
  271. package/core/server/data/seeders/importers/tags-importer.js +2 -2
  272. package/core/server/data/seeders/importers/users-importer.js +2 -2
  273. package/core/server/data/seeders/importers/web-mentions-importer.js +2 -2
  274. package/core/server/data/seeders/utils/random.js +7 -0
  275. package/core/server/ghost-server.js +19 -5
  276. package/core/server/lib/db-date.js +72 -3
  277. package/core/server/models/gift-delivery.js +23 -0
  278. package/core/server/models/gift.js +9 -0
  279. package/core/server/models/index.js +3 -2
  280. package/core/server/models/settings.js +43 -4
  281. package/core/server/services/automations/database-automations-repository.js +38 -31
  282. package/core/server/services/automations/poll.js +1 -1
  283. package/core/server/services/content-import/csv/index.js +8 -0
  284. package/core/server/services/content-import/csv/parse.js +60 -0
  285. package/core/server/services/content-import/import/importer.js +222 -0
  286. package/core/server/services/content-import/import/post-data.js +52 -0
  287. package/core/server/services/content-import/import/reader.js +15 -0
  288. package/core/server/services/content-import/import/row.js +50 -0
  289. package/core/server/services/content-import/import/store.js +71 -0
  290. package/core/server/services/content-import/index.js +67 -0
  291. package/core/server/services/email-analytics/automation-email-analytics-batch-processor.js +1 -1
  292. package/core/server/services/email-analytics/index.js +3 -1
  293. package/core/server/services/email-analytics/jobs/index.js +2 -2
  294. package/core/server/services/email-service/email-service-wrapper.js +1 -1
  295. package/core/server/services/email-suppression-list/email-suppression-list.js +4 -2
  296. package/core/server/services/gifts/CONTEXT.md +27 -3
  297. package/core/server/services/gifts/README.md +8 -4
  298. package/core/server/services/gifts/constants.js +2 -1
  299. package/core/server/services/gifts/email-templates/gift-delivery.hbs +20 -0
  300. package/core/server/services/gifts/email-templates/gift-delivery.js +39 -0
  301. package/core/server/services/gifts/email-templates/gift-purchase-confirmation.hbs +2 -2
  302. package/core/server/services/gifts/email-templates/gift-purchase-confirmation.js +19 -4
  303. package/core/server/services/gifts/events/send-gift-delivery-event.js +15 -0
  304. package/core/server/services/gifts/gift-bookshelf-repository.js +23 -3
  305. package/core/server/services/gifts/gift-checkout-adapter.js +5 -3
  306. package/core/server/services/gifts/gift-codec.js +8 -0
  307. package/core/server/services/gifts/gift-delivery-bookshelf-repository.js +107 -0
  308. package/core/server/services/gifts/gift-delivery-codec.js +12 -0
  309. package/core/server/services/gifts/gift-delivery-schema.js +15 -0
  310. package/core/server/services/gifts/gift-delivery-service.js +209 -0
  311. package/core/server/services/gifts/gift-email-renderer.js +13 -1
  312. package/core/server/services/gifts/gift-email-service.js +97 -17
  313. package/core/server/services/gifts/gift-schema.js +10 -6
  314. package/core/server/services/gifts/gift-service.js +259 -34
  315. package/core/server/services/gifts/gift.js +59 -5
  316. package/core/server/services/gifts/index.js +69 -3
  317. package/core/server/services/lib/mailgun-client.js +5 -2
  318. package/core/server/services/mail/ghost-mailer.js +9 -1
  319. package/core/server/services/member-welcome-emails/service.js +6 -1
  320. package/core/server/services/members/members-api/controllers/router-controller.js +26 -8
  321. package/core/server/services/stripe/services/webhook/checkout-session-event-service.js +100 -12
  322. package/core/server/services/stripe/stripe-api.js +3 -2
  323. package/core/server/services/stripe/webhook-controller.js +6 -3
  324. package/core/server/services/stripe/webhook-manager.js +2 -0
  325. package/core/shared/labs.js +1 -0
  326. package/package.json +9 -8
  327. package/pnpm-lock.yaml +26 -11
  328. package/pnpm-workspace.yaml +5 -6
  329. package/content/themes/source/yarn.lock +0 -6232
  330. package/core/built/admin/assets/Combination-BDvCdg1t.js +0 -41
  331. package/core/built/admin/assets/about-B_BvSyLF.js +0 -1
  332. package/core/built/admin/assets/account-migration-oJUGxnzT.js +0 -1
  333. package/core/built/admin/assets/activitypub/dist-Dqggkccr.js +0 -215
  334. package/core/built/admin/assets/activitypub/inbox-z1o2j1Tb.js +0 -1221
  335. package/core/built/admin/assets/activitypub/layout-BRMezuty.js +0 -1
  336. package/core/built/admin/assets/activitypub/moderation-CZudJi8N.js +0 -160
  337. package/core/built/admin/assets/activitypub/settings-CrSV1cux.js +0 -107
  338. package/core/built/admin/assets/add-integration-modal-BW23PDYs.js +0 -1
  339. package/core/built/admin/assets/add-newsletter-modal-M0ls8BKO.js +0 -1
  340. package/core/built/admin/assets/add-recommendation-modal-B0NeBMzl.js +0 -1
  341. package/core/built/admin/assets/announcement-bar-modal-pXBqjQE_.js +0 -1
  342. package/core/built/admin/assets/api-keys-Df6uv1dT.js +0 -1
  343. package/core/built/admin/assets/audience-BY8od8If.js +0 -1
  344. package/core/built/admin/assets/automations-DCs6Wady.js +0 -1
  345. package/core/built/admin/assets/bluesky-sharing-D0P42Cws.js +0 -1
  346. package/core/built/admin/assets/breadcrumb-MTj_cKLb.js +0 -1
  347. package/core/built/admin/assets/chart-helpers-DcO3k_vA.js +0 -1
  348. package/core/built/admin/assets/chunk-KS7C4IRE-DG9HTHQ3.js +0 -4
  349. package/core/built/admin/assets/color-picker-CBlJMv3y.js +0 -3
  350. package/core/built/admin/assets/combobox-BbPJzjCp.js +0 -1
  351. package/core/built/admin/assets/command-BrhXIlDr.js +0 -1
  352. package/core/built/admin/assets/comments-Cu4rD1aJ.js +0 -1
  353. package/core/built/admin/assets/confirmation-provider-BEdrnHns.js +0 -1
  354. package/core/built/admin/assets/custom-field-type-option-CNhAgHbJ.js +0 -1
  355. package/core/built/admin/assets/custom-integration-modal-CrGOI8IB.js +0 -1
  356. package/core/built/admin/assets/date-range-select-Bsh4gIek.js +0 -1
  357. package/core/built/admin/assets/design-and-theme-modal-DWZguwWX.js +0 -23
  358. package/core/built/admin/assets/dirty-confirm-dialog-DmdMpgyN.js +0 -1
  359. package/core/built/admin/assets/disabled-sources-indicator-BLnmyWgt.js +0 -1
  360. package/core/built/admin/assets/dist-DMFGvqqU.js +0 -1
  361. package/core/built/admin/assets/dist-DlCyLrOC.js +0 -1
  362. package/core/built/admin/assets/dist-j1xxxnY8.js +0 -1
  363. package/core/built/admin/assets/domain-Dp9TRrFp.js +0 -1
  364. package/core/built/admin/assets/edit-profile-DG6lpMsr.js +0 -1
  365. package/core/built/admin/assets/editor-CPQOrhrn.js +0 -8
  366. package/core/built/admin/assets/embed-signup-form-modal-CgWniDha.js +0 -9
  367. package/core/built/admin/assets/explore-C3rzONJv.js +0 -1
  368. package/core/built/admin/assets/feed-DpUwS1cU.js +0 -1
  369. package/core/built/admin/assets/filters-DZJT29GW.js +0 -1
  370. package/core/built/admin/assets/first-promoter-modal-DJhQQheG.js +0 -1
  371. package/core/built/admin/assets/gift-link-modal-sdFLjND9.js +0 -1
  372. package/core/built/admin/assets/global-data-provider-CixmTX00.js +0 -1
  373. package/core/built/admin/assets/growth-0YYkBEeS.js +0 -1
  374. package/core/built/admin/assets/growth-DpjAPeQT.js +0 -3
  375. package/core/built/admin/assets/history-modal-BWJ4-cJ0.js +0 -1
  376. package/core/built/admin/assets/html-field-C-oa5DEl.js +0 -1
  377. package/core/built/admin/assets/iframe-buffering-Ba9sDpi2.js +0 -1
  378. package/core/built/admin/assets/index-BOJzlYiz.js +0 -67
  379. package/core/built/admin/assets/index-Bid6LAyc.css +0 -1
  380. package/core/built/admin/assets/invite-user-modal-DCiQSiuD.js +0 -1
  381. package/core/built/admin/assets/koenig-editor-base-BApy8Q7C.js +0 -1
  382. package/core/built/admin/assets/kpi-helpers-ssbuy-hD.js +0 -1
  383. package/core/built/admin/assets/kpi-tabs-BqoIM6bI.js +0 -1
  384. package/core/built/admin/assets/label-picker-Cvvbmzb5.js +0 -1
  385. package/core/built/admin/assets/layout-DkkGyev2.js +0 -1
  386. package/core/built/admin/assets/member-custom-fields-CvGcpaiW.js +0 -1
  387. package/core/built/admin/assets/member-detail-CMCMZ47_.js +0 -1
  388. package/core/built/admin/assets/members-sRl6fNfq.js +0 -11
  389. package/core/built/admin/assets/moderation-KhKwGqI1.js +0 -1
  390. package/core/built/admin/assets/multi-select-combobox-nhCff0_K.js +0 -1
  391. package/core/built/admin/assets/navigation-modal-MDJExWDJ.js +0 -1
  392. package/core/built/admin/assets/newsletter-BIBHumfV.js +0 -1
  393. package/core/built/admin/assets/newsletter-detail-modal-CfjBMH3v.js +0 -1
  394. package/core/built/admin/assets/newsletter-emails-ClDL-7dA.js +0 -1
  395. package/core/built/admin/assets/newsletters-CBe-IPzK.js +0 -1
  396. package/core/built/admin/assets/note-HnNUOt4d.js +0 -1
  397. package/core/built/admin/assets/notifications-Cj5e-258.js +0 -1
  398. package/core/built/admin/assets/offers-container-modal-BHwHtixC.js +0 -1
  399. package/core/built/admin/assets/onboarding-route-D_4jp7Nq.js +0 -1
  400. package/core/built/admin/assets/overview-CezDdg-F.js +0 -1
  401. package/core/built/admin/assets/overview-D5mM51Lj.js +0 -1
  402. package/core/built/admin/assets/pintura-modal-BXXJ8Iaa.js +0 -1
  403. package/core/built/admin/assets/portal-frame-BLCvF8f-.js +0 -1
  404. package/core/built/admin/assets/portal-modal-DN80tAcy.js +0 -1
  405. package/core/built/admin/assets/post-analytics-header-_ydSY8qO.js +0 -1
  406. package/core/built/admin/assets/post-share-modal-CzYak_a5.js +0 -1
  407. package/core/built/admin/assets/preferences-gneLb6LV.js +0 -5
  408. package/core/built/admin/assets/preview-modal-Bkq-tgVm.js +0 -1
  409. package/core/built/admin/assets/profile-CzXmCNTC.js +0 -1
  410. package/core/built/admin/assets/radio-group-Dsq5ZEbV.js +0 -1
  411. package/core/built/admin/assets/router-provider-CkB_ArLF.js +0 -1
  412. package/core/built/admin/assets/select-xuZAuDyS.js +0 -1
  413. package/core/built/admin/assets/settings-Ct8pabhr.js +0 -1
  414. package/core/built/admin/assets/settings-KSWWQanQ.js +0 -4
  415. package/core/built/admin/assets/settings-breadcrumbs-DbPGu-Tu.js +0 -1
  416. package/core/built/admin/assets/share-modal-CdiUx9WM.js +0 -1
  417. package/core/built/admin/assets/slack-modal-BP--Z3jE.js +0 -1
  418. package/core/built/admin/assets/sort-button-CjfKdlsL.js +0 -1
  419. package/core/built/admin/assets/source-icon-DHj1h6Ts.js +0 -1
  420. package/core/built/admin/assets/stats-C615kulJ.js +0 -1
  421. package/core/built/admin/assets/stats-view-BSMRdYiN.js +0 -1
  422. package/core/built/admin/assets/step-1-pSdGYtIF.js +0 -1
  423. package/core/built/admin/assets/step-2-T9Uy4eBD.js +0 -1
  424. package/core/built/admin/assets/step-3-D4uXe_XG.js +0 -2
  425. package/core/built/admin/assets/stripe-connect-modal-C2vTvepi.js +0 -1
  426. package/core/built/admin/assets/switch-CE2NuMn-.js +0 -1
  427. package/core/built/admin/assets/tabs-l9c7FjzI.js +0 -1
  428. package/core/built/admin/assets/tag-detail-DBw_M2-p.js +0 -2
  429. package/core/built/admin/assets/tags-DoWsAG6l.js +0 -1
  430. package/core/built/admin/assets/testimonials-modal-BZKePE2T.js +0 -1
  431. package/core/built/admin/assets/tier-detail-modal-D65pVWsN.js +0 -1
  432. package/core/built/admin/assets/toggle-group-uJnl42w3.js +0 -1
  433. package/core/built/admin/assets/topic-filter-BM7hz0kW.js +0 -1
  434. package/core/built/admin/assets/transistor-modal-VZOGN3od.js +0 -1
  435. package/core/built/admin/assets/unsplash-modal-CKGfACxF.js +0 -1
  436. package/core/built/admin/assets/use-check-theme-limit-error-CGp3CGT5.js +0 -1
  437. package/core/built/admin/assets/use-feature-flag-CSq2Lwzt.js +0 -1
  438. package/core/built/admin/assets/use-filter-params-0hcJJuEJ.js +0 -1
  439. package/core/built/admin/assets/use-limiter-ffwpsZPp.js +0 -2
  440. package/core/built/admin/assets/use-pintura-editor-Cb41oJyL.js +0 -1
  441. package/core/built/admin/assets/use-query-params-DHhhu8wi.js +0 -1
  442. package/core/built/admin/assets/use-reply-chain-data-C0ZUTkfk.js +0 -1
  443. package/core/built/admin/assets/use-setting-group-Bx8M6FL2.js +0 -1
  444. package/core/built/admin/assets/use-settings-navigation-BOZcat_V.js +0 -1
  445. package/core/built/admin/assets/use-upgrade-route-B3_DUiuT.js +0 -1
  446. package/core/built/admin/assets/user-detail-modal-595z1N5r.js +0 -1
  447. package/core/built/admin/assets/web-CAta3lRE.js +0 -1
  448. package/core/built/admin/assets/web-Ds9_wWJK.js +0 -2
  449. package/core/built/admin/assets/zapier-modal-CcfDZY53.js +0 -1
  450. package/core/server/data/seeders/utils/database-date.js +0 -28
  451. package/core/server/services/automations/database-date.js +0 -59
  452. /package/core/built/admin/assets/{dist-CqO2lWS02.js → dist-CqO2lWS0.js} +0 -0
  453. /package/core/server/services/{automations → lib}/mailgun-message-id.js +0 -0
@@ -11,6 +11,9 @@ exports.giftCodec = zod_1.z.codec(gift_schema_1.DbGift, zod_1.z.instanceof(gift_
11
11
  token: row.token,
12
12
  buyerEmail: row.buyer_email,
13
13
  buyerMemberId: row.buyer_member_id,
14
+ buyerName: row.buyer_name,
15
+ recipientName: row.recipient_name,
16
+ personalMessage: row.personal_message,
14
17
  redeemerMemberId: row.redeemer_member_id,
15
18
  tierId: row.tier_id,
16
19
  cadence: row.cadence,
@@ -19,6 +22,7 @@ exports.giftCodec = zod_1.z.codec(gift_schema_1.DbGift, zod_1.z.instanceof(gift_
19
22
  amount: row.amount,
20
23
  stripeCheckoutSessionId: row.stripe_checkout_session_id,
21
24
  stripePaymentIntentId: row.stripe_payment_intent_id,
25
+ checkoutStartedAt: row.checkout_started_at,
22
26
  consumesAt: row.consumes_at,
23
27
  expiresAt: row.expires_at,
24
28
  status: row.status,
@@ -33,6 +37,9 @@ exports.giftCodec = zod_1.z.codec(gift_schema_1.DbGift, zod_1.z.instanceof(gift_
33
37
  token: gift.token,
34
38
  buyer_email: gift.buyerEmail,
35
39
  buyer_member_id: gift.buyerMemberId,
40
+ buyer_name: gift.buyerName,
41
+ recipient_name: gift.recipientName,
42
+ personal_message: gift.personalMessage,
36
43
  redeemer_member_id: gift.redeemerMemberId,
37
44
  tier_id: gift.tierId,
38
45
  cadence: gift.cadence,
@@ -41,6 +48,7 @@ exports.giftCodec = zod_1.z.codec(gift_schema_1.DbGift, zod_1.z.instanceof(gift_
41
48
  amount: gift.amount,
42
49
  stripe_checkout_session_id: gift.stripeCheckoutSessionId,
43
50
  stripe_payment_intent_id: gift.stripePaymentIntentId,
51
+ checkout_started_at: gift.checkoutStartedAt,
44
52
  consumes_at: gift.consumesAt,
45
53
  expires_at: gift.expiresAt,
46
54
  status: gift.status,
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GiftDeliveryBookshelfRepository = void 0;
4
+ const db_date_1 = require("../../lib/db-date");
5
+ const gift_delivery_codec_1 = require("./gift-delivery-codec");
6
+ // Deliveries that still need a send attempt: never started, or claimed by a
7
+ // process that has since died and left the claim stale
8
+ function recoverableDeliveries(query, staleBefore) {
9
+ query.andWhere((recoverable) => {
10
+ recoverable.where('gift_deliveries.status', 'pending')
11
+ .orWhere((stale) => {
12
+ stale.where('gift_deliveries.status', 'sending')
13
+ .where('gift_deliveries.started_at', '<=', (0, db_date_1.toDatabaseDate)(staleBefore));
14
+ });
15
+ });
16
+ }
17
+ class GiftDeliveryBookshelfRepository {
18
+ model;
19
+ knex;
20
+ constructor({ GiftDeliveryModel, knex }) {
21
+ this.model = GiftDeliveryModel;
22
+ this.knex = knex;
23
+ }
24
+ async getById(id, options = {}) {
25
+ const model = await this.model.findOne({ id }, { require: false, ...options });
26
+ return model ? (0, gift_delivery_codec_1.decodeGiftDeliveryRow)(model.toJSON()) : null;
27
+ }
28
+ async getByGiftId(giftId, options = {}) {
29
+ const model = await this.model.findOne({ gift_id: giftId }, { require: false, ...options });
30
+ return model ? (0, gift_delivery_codec_1.decodeGiftDeliveryRow)(model.toJSON()) : null;
31
+ }
32
+ async findRecoverableForPurchasedGifts(staleBefore, limit) {
33
+ const rows = await this.knex('gift_deliveries')
34
+ .select('gift_deliveries.*')
35
+ .join('gifts', 'gifts.id', 'gift_deliveries.gift_id')
36
+ .where('gifts.status', 'purchased')
37
+ .modify(recoverableDeliveries, staleBefore)
38
+ .limit(limit);
39
+ return rows.map(gift_delivery_codec_1.decodeGiftDeliveryRow);
40
+ }
41
+ async tryStartDelivery(id, now, staleBefore) {
42
+ const claimed = await this.knex('gift_deliveries')
43
+ .where({ id })
44
+ .whereExists((query) => {
45
+ query.select('gifts.id')
46
+ .from('gifts')
47
+ .whereRaw('gifts.id = gift_deliveries.gift_id')
48
+ .where('gifts.status', 'purchased');
49
+ })
50
+ .modify(recoverableDeliveries, staleBefore)
51
+ .update({
52
+ status: 'sending',
53
+ started_at: (0, db_date_1.toDatabaseDate)(now)
54
+ });
55
+ if (claimed !== 1) {
56
+ return null;
57
+ }
58
+ return this.getById(id);
59
+ }
60
+ async markSent(id, sentAt, providerMessageId) {
61
+ return this.updateState(id, 'sending', {
62
+ status: 'sent',
63
+ email_sent_at: (0, db_date_1.toDatabaseDate)(sentAt),
64
+ email_provider_message_id: providerMessageId,
65
+ started_at: null
66
+ });
67
+ }
68
+ // A delivery cancelled while its email was in flight keeps the acceptance
69
+ // details so the message can still be correlated with transport outcomes
70
+ async recordCancelledAcceptance(id, sentAt, providerMessageId) {
71
+ return this.updateState(id, 'cancelled', {
72
+ email_sent_at: (0, db_date_1.toDatabaseDate)(sentAt),
73
+ email_provider_message_id: providerMessageId
74
+ });
75
+ }
76
+ async markFailed(id) {
77
+ return this.updateState(id, 'sending', {
78
+ status: 'failed',
79
+ started_at: null
80
+ });
81
+ }
82
+ async markCancelled(id) {
83
+ return this.updateState(id, 'sending', {
84
+ status: 'cancelled',
85
+ started_at: null
86
+ });
87
+ }
88
+ async cancelPendingForGift(token, options = {}) {
89
+ const db = options.transacting ?? this.knex;
90
+ const gift = db('gifts').select('id').where({ token });
91
+ const updated = await db('gift_deliveries')
92
+ .whereIn('status', ['pending', 'sending'])
93
+ .whereIn('gift_id', gift)
94
+ .update({ status: 'cancelled', started_at: null });
95
+ return updated === 1;
96
+ }
97
+ async create(delivery, options = {}) {
98
+ await this.model.add((0, gift_delivery_codec_1.encodeGiftDelivery)(delivery), options);
99
+ }
100
+ async updateState(id, from, data) {
101
+ const updated = await this.knex('gift_deliveries')
102
+ .where({ id, status: from })
103
+ .update(data);
104
+ return updated === 1;
105
+ }
106
+ }
107
+ exports.GiftDeliveryBookshelfRepository = GiftDeliveryBookshelfRepository;
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.decodeGiftDeliveryRow = decodeGiftDeliveryRow;
4
+ exports.encodeGiftDelivery = encodeGiftDelivery;
5
+ const case_keys_1 = require("../../lib/case-keys");
6
+ const gift_delivery_schema_1 = require("./gift-delivery-schema");
7
+ function decodeGiftDeliveryRow(input) {
8
+ return (0, case_keys_1.camelKeys)(gift_delivery_schema_1.DbGiftDelivery.parse(input));
9
+ }
10
+ function encodeGiftDelivery(delivery) {
11
+ return gift_delivery_schema_1.DbGiftDelivery.parse((0, case_keys_1.snakeKeys)(delivery));
12
+ }
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DbGiftDelivery = exports.GiftDeliveryStatusSchema = void 0;
4
+ const zod_1 = require("zod");
5
+ const db_date_1 = require("../../lib/db-date");
6
+ exports.GiftDeliveryStatusSchema = zod_1.z.enum(['pending', 'sending', 'sent', 'failed', 'cancelled']);
7
+ exports.DbGiftDelivery = zod_1.z.object({
8
+ id: zod_1.z.string(),
9
+ gift_id: zod_1.z.string(),
10
+ recipient_email: zod_1.z.string().email(),
11
+ status: exports.GiftDeliveryStatusSchema.default('pending'),
12
+ started_at: db_date_1.DbDate.nullable().default(null),
13
+ email_sent_at: db_date_1.DbDate.nullable().default(null),
14
+ email_provider_message_id: zod_1.z.string().nullable().default(null)
15
+ });
@@ -0,0 +1,209 @@
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.GiftDeliveryService = void 0;
7
+ const bson_objectid_1 = __importDefault(require("bson-objectid"));
8
+ const logging_1 = __importDefault(require("@tryghost/logging"));
9
+ const send_gift_delivery_event_1 = require("./events/send-gift-delivery-event");
10
+ const constants_1 = require("./constants");
11
+ const DomainEvents = require('@tryghost/domain-events');
12
+ class GiftDeliveryService {
13
+ deps;
14
+ constructor(deps) {
15
+ this.deps = deps;
16
+ }
17
+ async createForCheckout({ giftId, recipientEmail }, options = {}) {
18
+ const delivery = {
19
+ id: new bson_objectid_1.default().toHexString(),
20
+ giftId,
21
+ recipientEmail,
22
+ status: 'pending',
23
+ startedAt: null,
24
+ emailSentAt: null,
25
+ emailProviderMessageId: null
26
+ };
27
+ await this.deps.giftDeliveryRepository.create(delivery, options);
28
+ return delivery.id;
29
+ }
30
+ async dispatchForGift(giftId) {
31
+ const delivery = await this.deps.giftDeliveryRepository.getByGiftId(giftId);
32
+ if (!delivery || delivery.status !== 'pending') {
33
+ return delivery?.recipientEmail ?? null;
34
+ }
35
+ DomainEvents.dispatch(send_gift_delivery_event_1.SendGiftDeliveryEvent.create({ deliveryId: delivery.id }));
36
+ return delivery.recipientEmail;
37
+ }
38
+ async recoverPending(limit = 1000) {
39
+ const staleBefore = new Date(Date.now() - constants_1.GIFT_DELIVERY_STALE_AFTER_MS);
40
+ const deliveries = await this.deps.giftDeliveryRepository.findRecoverableForPurchasedGifts(staleBefore, limit);
41
+ const result = {
42
+ sentCount: 0,
43
+ skippedCount: 0,
44
+ failedCount: 0
45
+ };
46
+ // Sequential on purpose: recovery can find many deliveries at once and each
47
+ // send holds a mail transport call, so fanning out through events would open
48
+ // them all concurrently
49
+ for (const delivery of deliveries) {
50
+ try {
51
+ const deliveryResult = await this.send(delivery.id);
52
+ result[`${deliveryResult}Count`] += 1;
53
+ }
54
+ catch (err) {
55
+ result.failedCount += 1;
56
+ logging_1.default.error({
57
+ event: { name: 'gift_delivery.recovery_failed' },
58
+ err,
59
+ deliveryId: delivery.id
60
+ }, 'Failed to recover gift delivery');
61
+ }
62
+ }
63
+ return result;
64
+ }
65
+ async cancelPendingForGift(token, options = {}) {
66
+ return this.deps.giftDeliveryRepository.cancelPendingForGift(token, options);
67
+ }
68
+ async send(id) {
69
+ const now = new Date();
70
+ const staleBefore = new Date(now.getTime() - constants_1.GIFT_DELIVERY_STALE_AFTER_MS);
71
+ const delivery = await this.deps.giftDeliveryRepository.tryStartDelivery(id, now, staleBefore);
72
+ if (!delivery) {
73
+ return 'skipped';
74
+ }
75
+ const gift = await this.deps.giftRepository.getById(delivery.giftId);
76
+ if (!gift) {
77
+ logging_1.default.error({
78
+ event: { name: 'gift_delivery.gift_missing' },
79
+ deliveryId: delivery.id,
80
+ giftId: delivery.giftId
81
+ }, 'Started gift delivery has no gift');
82
+ await this.deps.giftDeliveryRepository.markFailed(delivery.id);
83
+ return 'failed';
84
+ }
85
+ if (gift.status !== 'purchased') {
86
+ await this.deps.giftDeliveryRepository.markCancelled(delivery.id);
87
+ return 'skipped';
88
+ }
89
+ if (!gift.buyerEmail || !gift.buyerName) {
90
+ logging_1.default.error({
91
+ event: { name: 'gift_delivery.buyer_details_missing' },
92
+ deliveryId: delivery.id,
93
+ giftId: delivery.giftId
94
+ }, 'Gift delivery is missing required buyer details');
95
+ await this.deps.giftDeliveryRepository.markFailed(delivery.id);
96
+ return 'failed';
97
+ }
98
+ let tier;
99
+ try {
100
+ tier = await this.deps.tiersService.api.read(gift.tierId);
101
+ }
102
+ catch (err) {
103
+ logging_1.default.error({
104
+ event: { name: 'gift_delivery.tier_read_failed' },
105
+ err,
106
+ deliveryId: delivery.id,
107
+ giftId: delivery.giftId,
108
+ tierId: gift.tierId
109
+ }, 'Failed to read tier for gift delivery');
110
+ await this.deps.giftDeliveryRepository.markFailed(delivery.id);
111
+ return 'failed';
112
+ }
113
+ if (!tier) {
114
+ logging_1.default.error({
115
+ event: { name: 'gift_delivery.tier_missing' },
116
+ deliveryId: delivery.id,
117
+ giftId: delivery.giftId,
118
+ tierId: gift.tierId
119
+ }, 'Tier not found for gift delivery');
120
+ await this.deps.giftDeliveryRepository.markFailed(delivery.id);
121
+ return 'failed';
122
+ }
123
+ let result;
124
+ try {
125
+ result = await this.deps.giftEmailService.sendGiftDelivery({
126
+ recipientEmail: delivery.recipientEmail,
127
+ recipientName: gift.recipientName,
128
+ buyerEmail: gift.buyerEmail,
129
+ buyerName: gift.buyerName,
130
+ personalMessage: gift.personalMessage,
131
+ token: gift.token,
132
+ tierName: tier.name,
133
+ benefits: tier.toJSON().benefits,
134
+ cadence: gift.cadence,
135
+ duration: gift.duration,
136
+ expiresAt: gift.expiresAt
137
+ });
138
+ }
139
+ catch (err) {
140
+ logging_1.default.error({
141
+ event: { name: 'gift_delivery.acceptance_failed' },
142
+ // The bulk mailer rejects with {error, messageData}; the rendered
143
+ // message and recipient must stay out of the logs
144
+ err: isMailgunRejection(err) ? err.error : err,
145
+ deliveryId: delivery.id,
146
+ giftId: delivery.giftId
147
+ }, 'Mail transport did not accept gift delivery');
148
+ await this.deps.giftDeliveryRepository.markFailed(delivery.id);
149
+ return 'failed';
150
+ }
151
+ const sentAt = new Date();
152
+ let persisted;
153
+ let cancelledDuringSend = false;
154
+ try {
155
+ persisted = await this.deps.giftDeliveryRepository.markSent(delivery.id, sentAt, result.providerMessageId);
156
+ }
157
+ catch (err) {
158
+ logging_1.default.warn({
159
+ event: { name: 'gift_delivery.acceptance_persistence.retrying' },
160
+ err,
161
+ deliveryId: delivery.id
162
+ }, 'Retrying accepted gift delivery persistence');
163
+ try {
164
+ persisted = await this.deps.giftDeliveryRepository.markSent(delivery.id, sentAt, result.providerMessageId);
165
+ }
166
+ catch (retryErr) {
167
+ logging_1.default.error({
168
+ event: { name: 'gift_delivery.acceptance_persistence.failed' },
169
+ err: retryErr,
170
+ deliveryId: delivery.id
171
+ }, 'Failed to persist accepted gift delivery');
172
+ try {
173
+ await this.deps.giftDeliveryRepository.markFailed(delivery.id);
174
+ }
175
+ catch (markFailedErr) {
176
+ logging_1.default.error({
177
+ event: { name: 'gift_delivery.acceptance_terminal_persistence.failed' },
178
+ err: markFailedErr,
179
+ deliveryId: delivery.id
180
+ }, 'Failed to move accepted gift delivery out of recovery');
181
+ }
182
+ return 'failed';
183
+ }
184
+ }
185
+ if (!persisted) {
186
+ cancelledDuringSend = await this.deps.giftDeliveryRepository.recordCancelledAcceptance(delivery.id, sentAt, result.providerMessageId);
187
+ }
188
+ if (persisted) {
189
+ return 'sent';
190
+ }
191
+ if (cancelledDuringSend) {
192
+ logging_1.default.info({
193
+ event: { name: 'gift_delivery.cancelled_during_send' },
194
+ deliveryId: delivery.id,
195
+ giftId: delivery.giftId
196
+ }, 'Gift delivery was cancelled while its email was in flight');
197
+ return 'skipped';
198
+ }
199
+ logging_1.default.error({
200
+ event: { name: 'gift_delivery.acceptance_persistence.failed' },
201
+ deliveryId: delivery.id
202
+ }, 'Failed to persist accepted gift delivery');
203
+ return 'failed';
204
+ }
205
+ }
206
+ exports.GiftDeliveryService = GiftDeliveryService;
207
+ function isMailgunRejection(err) {
208
+ return typeof err === 'object' && err !== null && 'error' in err && 'messageData' in err;
209
+ }
@@ -9,11 +9,13 @@ const node_path_1 = __importDefault(require("node:path"));
9
9
  const handlebars_1 = __importDefault(require("handlebars"));
10
10
  const gift_purchase_confirmation_1 = require("./email-templates/gift-purchase-confirmation");
11
11
  const gift_reminder_1 = require("./email-templates/gift-reminder");
12
+ const gift_delivery_1 = require("./email-templates/gift-delivery");
12
13
  class GiftEmailRenderer {
13
14
  handlebars;
14
15
  t;
15
16
  purchaseConfirmationTemplate = null;
16
17
  reminderTemplate = null;
18
+ deliveryTemplate = null;
17
19
  constructor({ t }) {
18
20
  this.t = t;
19
21
  this.handlebars = handlebars_1.default.create();
@@ -39,11 +41,21 @@ class GiftEmailRenderer {
39
41
  text: (0, gift_reminder_1.renderText)(data, this.t)
40
42
  };
41
43
  }
44
+ async renderDelivery(data) {
45
+ if (!this.deliveryTemplate) {
46
+ const source = await node_fs_1.promises.readFile(node_path_1.default.join(__dirname, './email-templates/gift-delivery.hbs'), 'utf8');
47
+ this.deliveryTemplate = this.handlebars.compile(source);
48
+ }
49
+ return {
50
+ html: this.deliveryTemplate(data),
51
+ text: (0, gift_delivery_1.renderText)(data, this.t)
52
+ };
53
+ }
42
54
  registerTemplateHelpers() {
43
55
  this.handlebars.registerHelper('t', (key, options) => {
44
56
  const hash = options?.hash || {};
45
57
  const escapedHash = Object.entries(hash).reduce((acc, [name, value]) => {
46
- acc[name] = this.htmlSafeInterpolationValue(value);
58
+ acc[name] = typeof value === 'number' ? value : this.htmlSafeInterpolationValue(value);
47
59
  return acc;
48
60
  }, {});
49
61
  return new handlebars_1.default.SafeString(this.t(key, {
@@ -1,18 +1,27 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.GiftEmailService = void 0;
4
7
  const gift_email_renderer_1 = require("./gift-email-renderer");
8
+ const color_utils_1 = require("@tryghost/color-utils");
9
+ const errors_1 = __importDefault(require("@tryghost/errors"));
10
+ const mailgun_message_id_1 = require("../lib/mailgun-message-id");
5
11
  const DEFAULT_DATE_LOCALE = 'en-gb';
12
+ const DEFAULT_ACCENT_COLOR = '#15212A';
6
13
  class GiftEmailService {
7
- mailer;
14
+ transactionalMailer;
15
+ bulkMailer;
8
16
  settingsCache;
9
17
  urlUtils;
10
18
  getFromAddress;
11
19
  blogIcon;
12
20
  renderer;
13
21
  t;
14
- constructor({ mailer, settingsCache, urlUtils, getFromAddress, blogIcon, t }) {
15
- this.mailer = mailer;
22
+ constructor({ transactionalMailer, bulkMailer, settingsCache, urlUtils, getFromAddress, blogIcon, t }) {
23
+ this.transactionalMailer = transactionalMailer;
24
+ this.bulkMailer = bulkMailer;
16
25
  this.settingsCache = settingsCache;
17
26
  this.urlUtils = urlUtils;
18
27
  this.getFromAddress = getFromAddress;
@@ -28,14 +37,22 @@ class GiftEmailService {
28
37
  return '';
29
38
  }
30
39
  }
31
- getCadenceLabel(cadence, duration) {
32
- if (duration === 1) {
33
- return cadence === 'year' ? this.t('one-year') : this.t('one-month');
40
+ get accentColor() {
41
+ return this.settingsCache.get('accent_color') || DEFAULT_ACCENT_COLOR;
42
+ }
43
+ mixAccentColor(target, accentWeight, fallback) {
44
+ try {
45
+ return (0, color_utils_1.Color)(target).mix((0, color_utils_1.Color)(this.accentColor), accentWeight).hex().toLowerCase();
34
46
  }
35
- if (cadence === 'year') {
36
- return this.t('{count} year', { count: duration });
47
+ catch {
48
+ return fallback;
37
49
  }
38
- return this.t('{count} month', { count: duration });
50
+ }
51
+ get accentTint() {
52
+ return this.mixAccentColor('#FFFFFF', 0.07, '#F4F5F6');
53
+ }
54
+ get accentShade() {
55
+ return this.mixAccentColor('#15212A', 0.72, '#738A94');
39
56
  }
40
57
  formatDate(date) {
41
58
  const locale = this.settingsCache.get('locale') || DEFAULT_DATE_LOCALE;
@@ -45,12 +62,11 @@ class GiftEmailService {
45
62
  year: 'numeric'
46
63
  }).format(date);
47
64
  }
48
- async sendPurchaseConfirmation({ buyerEmail, token, tierName, cadence, duration, expiresAt }) {
65
+ async sendPurchaseConfirmation({ buyerEmail, token, tierName, cadence, duration, expiresAt, recipientEmail = null }) {
49
66
  const siteDomain = this.siteDomain;
50
67
  const siteUrl = this.urlUtils.getSiteUrl();
51
68
  const siteTitle = this.settingsCache.get('title') ?? siteDomain;
52
69
  const giftLink = `${siteUrl.replace(/\/$/, '')}/gift/${token}`;
53
- const cadenceLabel = this.getCadenceLabel(cadence, duration);
54
70
  const { html, text } = await this.renderer.renderPurchaseConfirmation({
55
71
  siteTitle,
56
72
  siteUrl,
@@ -60,18 +76,21 @@ class GiftEmailService {
60
76
  toEmail: buyerEmail,
61
77
  gift: {
62
78
  tierName,
63
- cadenceLabel,
79
+ duration,
80
+ isMonthly: cadence === 'month',
64
81
  link: giftLink,
65
- expiresAt: this.formatDate(expiresAt)
82
+ expiresAt: this.formatDate(expiresAt),
83
+ recipientEmail
66
84
  }
67
85
  });
68
- await this.mailer.send({
86
+ await this.transactionalMailer.send({
69
87
  to: buyerEmail,
70
- subject: this.t('Your gift is ready'),
88
+ subject: recipientEmail ? this.t('Your gift is on its way') : this.t('Your gift is ready'),
71
89
  html,
72
90
  text,
73
91
  from: this.getFromAddress(),
74
- forceTextContent: true
92
+ forceTextContent: true,
93
+ disableTracking: true
75
94
  });
76
95
  }
77
96
  async sendReminder({ memberEmail, memberName, tierName, consumesAt }) {
@@ -94,7 +113,7 @@ class GiftEmailService {
94
113
  manageSubscriptionUrl
95
114
  }
96
115
  });
97
- await this.mailer.send({
116
+ await this.transactionalMailer.send({
98
117
  to: memberEmail,
99
118
  subject: this.t('Your gift subscription is ending soon'),
100
119
  html,
@@ -103,5 +122,66 @@ class GiftEmailService {
103
122
  forceTextContent: true
104
123
  });
105
124
  }
125
+ async sendGiftDelivery({ recipientEmail, recipientName, buyerEmail, buyerName, personalMessage, token, tierName, benefits, cadence, duration, expiresAt }) {
126
+ const siteDomain = this.siteDomain;
127
+ const siteUrl = this.urlUtils.getSiteUrl();
128
+ const siteTitle = this.settingsCache.get('title') ?? siteDomain;
129
+ const giftLink = `${siteUrl.replace(/\/$/, '')}/gift/${token}`;
130
+ const { html, text } = await this.renderer.renderDelivery({
131
+ siteTitle,
132
+ siteUrl,
133
+ siteIconUrl: this.blogIcon.getIconUrl({ absolute: true, fallbackToDefault: false }),
134
+ siteDomain,
135
+ accentColor: this.accentColor,
136
+ accentTint: this.accentTint,
137
+ accentShade: this.accentShade,
138
+ toEmail: recipientEmail,
139
+ buyerEmail,
140
+ buyerName,
141
+ recipientName,
142
+ personalMessage,
143
+ gift: {
144
+ tierName,
145
+ benefits,
146
+ duration,
147
+ isMonthly: cadence === 'month',
148
+ link: giftLink,
149
+ expiresAt: this.formatDate(expiresAt)
150
+ }
151
+ });
152
+ const subject = this.t('{buyerName} sent you a gift', {
153
+ buyerName,
154
+ interpolation: { escapeValue: false }
155
+ });
156
+ if (!this.bulkMailer.isConfigured()) {
157
+ await this.transactionalMailer.send({
158
+ to: recipientEmail,
159
+ subject,
160
+ html,
161
+ text,
162
+ from: this.getFromAddress(),
163
+ forceTextContent: true,
164
+ tags: ['gift-delivery'],
165
+ disableTracking: true
166
+ });
167
+ return { providerMessageId: null };
168
+ }
169
+ const response = await this.bulkMailer.send({
170
+ subject,
171
+ html,
172
+ plaintext: text,
173
+ from: this.getFromAddress(),
174
+ tags: ['gift-delivery'],
175
+ disable_tracking: true
176
+ }, { [recipientEmail]: {} }, []);
177
+ const providerMessageId = (0, mailgun_message_id_1.getMailgunMessageId)(response) ?? null;
178
+ if (!providerMessageId) {
179
+ throw new errors_1.default.EmailError({
180
+ message: 'Bulk Mailgun did not accept gift delivery',
181
+ code: 'EMAIL_NOT_ACCEPTED'
182
+ });
183
+ }
184
+ return { providerMessageId };
185
+ }
106
186
  }
107
187
  exports.GiftEmailService = GiftEmailService;
@@ -4,7 +4,7 @@ exports.DbGift = exports.GiftStatusSchema = exports.GiftCadenceSchema = void 0;
4
4
  const zod_1 = require("zod");
5
5
  const db_date_1 = require("../../lib/db-date");
6
6
  exports.GiftCadenceSchema = zod_1.z.enum(['month', 'year']);
7
- exports.GiftStatusSchema = zod_1.z.enum(['purchased', 'redeemed', 'consumed', 'expired', 'refunded']);
7
+ exports.GiftStatusSchema = zod_1.z.enum(['payment_pending', 'purchased', 'redeemed', 'consumed', 'expired', 'refunded']);
8
8
  /**
9
9
  * The persisted gift row. Bookshelf remains the persistence implementation, but
10
10
  * values read through it are still runtime data and are validated before they
@@ -12,20 +12,24 @@ exports.GiftStatusSchema = zod_1.z.enum(['purchased', 'redeemed', 'consumed', 'e
12
12
  */
13
13
  exports.DbGift = zod_1.z.object({
14
14
  token: zod_1.z.string(),
15
- buyer_email: zod_1.z.string(),
15
+ buyer_email: zod_1.z.string().nullable(),
16
16
  buyer_member_id: zod_1.z.string().nullable(),
17
+ buyer_name: zod_1.z.string().nullable().default(null),
18
+ recipient_name: zod_1.z.string().nullable().default(null),
19
+ personal_message: zod_1.z.string().nullable().default(null),
17
20
  redeemer_member_id: zod_1.z.string().nullable(),
18
21
  tier_id: zod_1.z.string(),
19
22
  cadence: exports.GiftCadenceSchema,
20
23
  duration: zod_1.z.number().int().nonnegative(),
21
24
  currency: zod_1.z.string(),
22
25
  amount: zod_1.z.number().int().nonnegative(),
23
- stripe_checkout_session_id: zod_1.z.string(),
24
- stripe_payment_intent_id: zod_1.z.string(),
26
+ stripe_checkout_session_id: zod_1.z.string().nullable(),
27
+ stripe_payment_intent_id: zod_1.z.string().nullable(),
28
+ checkout_started_at: db_date_1.DbDate.nullable().default(null),
25
29
  consumes_at: db_date_1.DbDate.nullable(),
26
- expires_at: db_date_1.DbDate,
30
+ expires_at: db_date_1.DbDate.nullable(),
27
31
  status: exports.GiftStatusSchema,
28
- purchased_at: db_date_1.DbDate,
32
+ purchased_at: db_date_1.DbDate.nullable(),
29
33
  redeemed_at: db_date_1.DbDate.nullable(),
30
34
  consumed_at: db_date_1.DbDate.nullable(),
31
35
  expired_at: db_date_1.DbDate.nullable(),