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
@@ -16,6 +16,10 @@ const gift_checkout_offer_1 = require("./gift-checkout-offer");
16
16
  const MS_PER_DAY = 24 * 60 * 60 * 1000;
17
17
  const GIFT_REMINDER_LEAD_MS = constants_1.GIFT_REMINDER_LEAD_DAYS * MS_PER_DAY;
18
18
  const GIFT_REMINDER_FLOOR_MS = constants_1.GIFT_REMINDER_FLOOR_DAYS * MS_PER_DAY;
19
+ const GIFT_NAME_MAX_LENGTH = 191;
20
+ const GIFT_EMAIL_MAX_LENGTH = 191;
21
+ const GIFT_CHECKOUT_MESSAGE_MAX_LENGTH = 250;
22
+ const GIFT_CHECKOUT_RETENTION_DAYS = 30;
19
23
  const errorMessages = {
20
24
  giftNotFound: 'This gift does not exist.',
21
25
  giftAlreadyRedeemed: 'This gift has already been redeemed.',
@@ -41,12 +45,84 @@ const GiftPurchaseDataSchema = zod_1.z.object({
41
45
  stripeCheckoutSessionId: zod_1.z.string().min(1),
42
46
  stripePaymentIntentId: zod_1.z.string().min(1)
43
47
  });
48
+ const StripeBuyerEmailSchema = zod_1.z.unknown().transform((value) => {
49
+ if (typeof value !== 'string') {
50
+ return null;
51
+ }
52
+ const email = value.trim();
53
+ return email && email.length <= GIFT_EMAIL_MAX_LENGTH ? email : null;
54
+ });
55
+ const GiftPaymentCompletionSchema = zod_1.z.object({
56
+ giftId: zod_1.z.string().min(1),
57
+ buyerEmail: StripeBuyerEmailSchema,
58
+ stripeCustomerId: zod_1.z.string().min(1).nullable(),
59
+ stripeCheckoutSessionId: zod_1.z.string().min(1),
60
+ stripePaymentIntentId: zod_1.z.string().min(1)
61
+ });
62
+ const NullableCheckoutStringSchema = (max) => zod_1.z.preprocess(value => typeof value === 'string' && value.trim() === '' ? null : value, zod_1.z.string().trim().max(max).nullable().optional().default(null));
63
+ const EmptyCheckoutStringSchema = zod_1.z.preprocess(value => typeof value === 'string' && value.trim() === '' ? null : value, zod_1.z.null().optional().default(null));
64
+ const RequiredCheckoutStringSchema = (max) => zod_1.z.string().trim().min(1).max(max);
65
+ const CheckoutBuyerEmailSchema = zod_1.z.string().trim().email().max(GIFT_EMAIL_MAX_LENGTH);
66
+ const GiftCheckoutDeliverySchema = zod_1.z.discriminatedUnion('deliveryMethod', [
67
+ zod_1.z.object({
68
+ deliveryMethod: zod_1.z.literal('link'),
69
+ recipientEmail: EmptyCheckoutStringSchema,
70
+ recipientName: EmptyCheckoutStringSchema,
71
+ personalMessage: EmptyCheckoutStringSchema,
72
+ buyerName: NullableCheckoutStringSchema(GIFT_NAME_MAX_LENGTH)
73
+ }),
74
+ zod_1.z.object({
75
+ deliveryMethod: zod_1.z.literal('email'),
76
+ recipientEmail: zod_1.z.string().trim().email().max(GIFT_EMAIL_MAX_LENGTH),
77
+ recipientName: NullableCheckoutStringSchema(GIFT_NAME_MAX_LENGTH),
78
+ personalMessage: NullableCheckoutStringSchema(GIFT_CHECKOUT_MESSAGE_MAX_LENGTH),
79
+ buyerName: RequiredCheckoutStringSchema(GIFT_NAME_MAX_LENGTH)
80
+ })
81
+ ]);
44
82
  class GiftService {
45
83
  deps;
46
84
  constructor(deps) {
47
85
  this.deps = deps;
48
86
  }
49
87
  async startCheckout(input) {
88
+ const customizationEnabled = this.deps.labsService.isSet('giftSubCustomization');
89
+ const populatedDeliveryFields = [
90
+ input.recipientEmail,
91
+ input.recipientName,
92
+ input.buyerName,
93
+ input.personalMessage
94
+ ].some(value => value !== undefined && value !== null && value !== '');
95
+ if (!customizationEnabled && (input.deliveryMethod === 'email' || populatedDeliveryFields)) {
96
+ throw new errors_1.default.BadRequestError({
97
+ message: 'Bad Request.',
98
+ context: 'Gift email delivery is not available'
99
+ });
100
+ }
101
+ const parsedBuyerEmail = CheckoutBuyerEmailSchema.safeParse(input.buyer.email);
102
+ if (customizationEnabled && !parsedBuyerEmail.success) {
103
+ throw new errors_1.default.BadRequestError({
104
+ message: 'Bad Request.',
105
+ context: `Invalid gift buyer email: ${parsedBuyerEmail.error.issues[0].message}`
106
+ });
107
+ }
108
+ const buyerEmail = parsedBuyerEmail.success ? parsedBuyerEmail.data : input.buyer.email;
109
+ const parsedDelivery = GiftCheckoutDeliverySchema.safeParse(customizationEnabled ? {
110
+ deliveryMethod: input.deliveryMethod ?? 'link',
111
+ recipientEmail: input.recipientEmail,
112
+ recipientName: input.recipientName,
113
+ buyerName: input.buyerName,
114
+ personalMessage: input.personalMessage
115
+ } : {
116
+ deliveryMethod: 'link'
117
+ });
118
+ if (!parsedDelivery.success) {
119
+ const issue = parsedDelivery.error.issues[0];
120
+ throw new errors_1.default.BadRequestError({
121
+ message: 'Bad Request.',
122
+ context: `Invalid gift delivery data: ${issue.message}`
123
+ });
124
+ }
125
+ const delivery = parsedDelivery.data;
50
126
  if (input.offerId) {
51
127
  throw new errors_1.default.BadRequestError({
52
128
  message: 'Bad Request.',
@@ -61,7 +137,7 @@ class GiftService {
61
137
  }
62
138
  let resolvedDuration = null;
63
139
  let cadence;
64
- if (this.deps.labsService.isSet('giftSubCustomization')) {
140
+ if (customizationEnabled) {
65
141
  resolvedDuration = (0, gift_checkout_offer_1.resolveGiftDuration)(input);
66
142
  cadence = resolvedDuration.cadence;
67
143
  }
@@ -115,32 +191,68 @@ class GiftService {
115
191
  successUrl.searchParams.set('gift_token', token);
116
192
  successUrl.searchParams.set('gift_tier', tierId);
117
193
  successUrl.searchParams.set('gift_cadence', cadence);
194
+ successUrl.searchParams.set('gift_delivery', delivery.deliveryMethod);
118
195
  if (totalMonths !== undefined) {
119
196
  successUrl.searchParams.set('gift_duration', String(totalMonths));
120
197
  }
121
- const customerId = input.buyer.isAuthenticated
122
- ? await this.deps.checkoutAdapter.getCustomerId(input.buyer)
198
+ const buyer = { ...input.buyer, email: buyerEmail };
199
+ const customerId = buyer.isAuthenticated
200
+ ? await this.deps.checkoutAdapter.getCustomerId(buyer)
123
201
  : null;
124
- const url = await this.deps.checkoutAdapter.createSession({
125
- amount,
126
- currency: tier.currency.toLowerCase(),
127
- tierName: tier.name,
202
+ const gift = gift_1.Gift.fromCheckout({
203
+ token,
204
+ buyerEmail,
205
+ buyerMemberId: buyer.isAuthenticated ? buyer.memberId : null,
206
+ buyerName: delivery.buyerName,
207
+ recipientName: delivery.recipientName,
208
+ personalMessage: delivery.personalMessage,
209
+ tierId,
128
210
  cadence,
129
211
  duration,
130
- metadata: {
131
- ...input.metadata,
132
- ghost_gift: 'true',
133
- gift_token: token,
134
- tier_id: tierId,
135
- cadence,
136
- duration: String(duration)
137
- },
138
- successUrl: successUrl.toString(),
139
- cancelUrl: input.cancelUrl,
140
- customerId,
141
- customerEmail: customerId ? null : input.buyer.email
212
+ currency: tier.currency.toLowerCase(),
213
+ amount
214
+ });
215
+ const giftId = await this.deps.giftRepository.transaction(async (transacting) => {
216
+ const id = await this.deps.giftRepository.create(gift, { transacting });
217
+ if (delivery.deliveryMethod === 'email') {
218
+ await this.deps.giftDeliveryService.createForCheckout({
219
+ giftId: id,
220
+ recipientEmail: delivery.recipientEmail
221
+ }, { transacting });
222
+ }
223
+ return id;
142
224
  });
143
- return { url };
225
+ let session;
226
+ try {
227
+ session = await this.deps.checkoutAdapter.createSession({
228
+ amount,
229
+ currency: tier.currency.toLowerCase(),
230
+ tierName: tier.name,
231
+ cadence,
232
+ duration,
233
+ metadata: { ghost_gift_id: giftId },
234
+ successUrl: successUrl.toString(),
235
+ cancelUrl: input.cancelUrl,
236
+ customerId,
237
+ customerEmail: customerId ? null : buyerEmail,
238
+ idempotencyKey: giftId
239
+ });
240
+ }
241
+ catch (err) {
242
+ try {
243
+ await this.deps.giftRepository.deletePendingCheckout(giftId);
244
+ }
245
+ catch (cleanupError) {
246
+ logging_1.default.error(cleanupError, `Failed to clean up gift checkout ${giftId} after Stripe session creation failed`);
247
+ }
248
+ throw err;
249
+ }
250
+ const bound = gift.bindCheckoutSession(session.id);
251
+ if (!bound) {
252
+ throw new errors_1.default.InternalServerError({ message: `Failed to bind checkout session to gift: ${giftId}` });
253
+ }
254
+ await this.deps.giftRepository.update(bound);
255
+ return { url: session.url };
144
256
  }
145
257
  generateToken() {
146
258
  /**
@@ -155,6 +267,82 @@ class GiftService {
155
267
  return token;
156
268
  }
157
269
  async completePurchase(input) {
270
+ if ('giftId' in input) {
271
+ return this.completePendingPurchase(input);
272
+ }
273
+ return this.completeLegacyPurchase(input);
274
+ }
275
+ async completePendingPurchase(input) {
276
+ const parsed = GiftPaymentCompletionSchema.safeParse(input);
277
+ if (!parsed.success) {
278
+ const issue = parsed.error.issues[0];
279
+ throw new errors_1.default.ValidationError({
280
+ message: 'Invalid gift purchase data.',
281
+ property: issue.path.join('.'),
282
+ context: issue.message
283
+ });
284
+ }
285
+ const data = parsed.data;
286
+ const member = data.stripeCustomerId
287
+ ? await this.deps.memberRepository.get({ customer_id: data.stripeCustomerId })
288
+ : null;
289
+ const completed = await this.deps.giftRepository.transaction(async (transacting) => {
290
+ const gift = await this.deps.giftRepository.getById(data.giftId, { transacting, forUpdate: true });
291
+ if (!gift) {
292
+ logging_1.default.error({
293
+ event: { name: 'gift_purchase.completion_gift_missing' },
294
+ giftId: data.giftId,
295
+ stripeCheckoutSessionId: data.stripeCheckoutSessionId,
296
+ stripePaymentIntentId: data.stripePaymentIntentId
297
+ }, 'Paid checkout completion has no matching gift');
298
+ return null;
299
+ }
300
+ if (gift.status !== 'payment_pending') {
301
+ return null;
302
+ }
303
+ if (gift.stripeCheckoutSessionId && gift.stripeCheckoutSessionId !== data.stripeCheckoutSessionId) {
304
+ throw new errors_1.default.ValidationError({ message: 'Checkout session does not match gift.' });
305
+ }
306
+ const buyerEmail = gift.buyerEmail ?? data.buyerEmail ?? member?.get('email') ?? null;
307
+ if (!buyerEmail) {
308
+ throw new errors_1.default.ValidationError({
309
+ message: 'Invalid gift purchase data.',
310
+ property: 'buyerEmail',
311
+ context: 'A purchased gift requires a buyer email'
312
+ });
313
+ }
314
+ // The pre-created gift owns the checkout price. Stripe's total may
315
+ // include automatic tax, so completion only adds settlement facts.
316
+ const purchased = gift.completePurchase({
317
+ buyerEmail,
318
+ buyerMemberId: member?.id ?? gift.buyerMemberId,
319
+ stripeCheckoutSessionId: data.stripeCheckoutSessionId,
320
+ stripePaymentIntentId: data.stripePaymentIntentId
321
+ });
322
+ if (!purchased) {
323
+ return null;
324
+ }
325
+ await this.deps.giftRepository.update(purchased, { transacting });
326
+ return purchased;
327
+ });
328
+ if (!completed) {
329
+ return false;
330
+ }
331
+ let recipientEmail = null;
332
+ try {
333
+ recipientEmail = await this.deps.giftDeliveryService.dispatchForGift(data.giftId);
334
+ }
335
+ catch (err) {
336
+ logging_1.default.error({
337
+ event: { name: 'gift_delivery.dispatch_failed' },
338
+ err,
339
+ giftId: data.giftId
340
+ }, 'Failed to dispatch purchased gift delivery');
341
+ }
342
+ await this.sendPurchaseNotifications(completed, member, recipientEmail);
343
+ return true;
344
+ }
345
+ async completeLegacyPurchase(input) {
158
346
  const parsed = GiftPurchaseDataSchema.safeParse(input);
159
347
  if (!parsed.success) {
160
348
  const issue = parsed.error.issues[0];
@@ -184,20 +372,44 @@ class GiftService {
184
372
  stripePaymentIntentId: data.stripePaymentIntentId
185
373
  });
186
374
  await this.deps.giftRepository.create(gift);
187
- const tier = await this.deps.tiersService.api.read(data.tierId);
375
+ await this.sendPurchaseNotifications(gift, member, null);
376
+ return true;
377
+ }
378
+ async sendPurchaseNotifications(gift, member, recipientEmail) {
379
+ let tier;
380
+ try {
381
+ tier = await this.deps.tiersService.api.read(gift.tierId);
382
+ }
383
+ catch (err) {
384
+ logging_1.default.error({
385
+ event: { name: 'gift_purchase_notifications.tier_read_failed' },
386
+ err,
387
+ tierId: gift.tierId
388
+ }, 'Failed to read tier for gift purchase notifications');
389
+ return;
390
+ }
188
391
  if (!tier) {
189
- throw new errors_1.default.NotFoundError({ message: `Tier not found: ${data.tierId}` });
392
+ logging_1.default.error({
393
+ event: { name: 'gift_purchase_notifications.tier_missing' },
394
+ tierId: gift.tierId
395
+ }, 'Tier not found for gift purchase notifications');
396
+ return;
397
+ }
398
+ const buyerEmail = gift.buyerEmail ?? member?.get('email') ?? null;
399
+ if (!buyerEmail) {
400
+ logging_1.default.warn('Skipping purchase notifications because the buyer email is unavailable');
401
+ return;
190
402
  }
191
403
  try {
192
404
  await this.deps.staffServiceEmails.notifyGiftPurchased({
193
405
  name: member?.get('name') ?? null,
194
- email: member?.get('email') ?? data.buyerEmail,
406
+ email: member?.get('email') ?? buyerEmail,
195
407
  memberId: member?.id ?? null,
196
- amount: data.amount,
197
- currency: data.currency,
408
+ amount: gift.amount,
409
+ currency: gift.currency,
198
410
  tierName: tier.name,
199
- cadence: data.cadence,
200
- duration: data.duration
411
+ cadence: gift.cadence,
412
+ duration: gift.duration
201
413
  });
202
414
  }
203
415
  catch (err) {
@@ -205,23 +417,25 @@ class GiftService {
205
417
  }
206
418
  try {
207
419
  await this.deps.giftEmailService.sendPurchaseConfirmation({
208
- buyerEmail: data.buyerEmail,
209
- token: data.token,
420
+ buyerEmail,
421
+ token: gift.token,
210
422
  tierName: tier.name,
211
- cadence: data.cadence,
212
- duration: data.duration,
213
- expiresAt: gift.expiresAt
423
+ cadence: gift.cadence,
424
+ duration: gift.duration,
425
+ expiresAt: gift.expiresAt,
426
+ recipientEmail
214
427
  });
215
428
  }
216
429
  catch (err) {
217
430
  logging_1.default.error('Failed to send gift purchase confirmation email', err);
218
431
  }
219
- return true;
220
432
  }
221
433
  assertRedeemable(gift, memberStatus) {
222
434
  const redeemableCheck = gift.checkRedeemable(memberStatus);
223
435
  if (!redeemableCheck.redeemable) {
224
436
  switch (redeemableCheck.reason) {
437
+ case 'payment-pending':
438
+ throw new errors_1.default.NotFoundError({ message: (0, tpl_1.default)(errorMessages.giftNotFound) });
225
439
  case 'redeemed':
226
440
  throw new errors_1.default.BadRequestError({
227
441
  message: (0, tpl_1.default)(errorMessages.giftAlreadyRedeemed),
@@ -332,6 +546,7 @@ class GiftService {
332
546
  status: 'gift'
333
547
  }, { id: memberId, transacting });
334
548
  await this.deps.giftRepository.update(redeemed, { transacting });
549
+ await this.deps.giftDeliveryService.cancelPendingForGift(redeemed.token, { transacting });
335
550
  // Gift members receive the paid welcome email, as they receive access to paid content
336
551
  await this.deps.memberRepository.triggerMemberSignupAutomation(memberId, member.get('email'), 'paid', { transacting });
337
552
  return { redeemed, member };
@@ -390,7 +605,7 @@ class GiftService {
390
605
  }
391
606
  async getPreview(token) {
392
607
  const gift = await this.deps.giftRepository.getByToken(token);
393
- if (!gift) {
608
+ if (!gift || gift.status === 'payment_pending') {
394
609
  return null;
395
610
  }
396
611
  const tier = await this.deps.tiersService.api.read(gift.tierId);
@@ -481,6 +696,7 @@ class GiftService {
481
696
  }
482
697
  await this.deps.giftRepository.transaction(async (transacting) => {
483
698
  await this.deps.giftRepository.update(refunded, { transacting });
699
+ await this.deps.giftDeliveryService.cancelPendingForGift(refunded.token, { transacting });
484
700
  if (gift.redeemerMemberId) {
485
701
  const member = await this.deps.memberRepository.get({ id: gift.redeemerMemberId }, { transacting });
486
702
  if (member?.get('status') === 'gift') {
@@ -562,11 +778,17 @@ class GiftService {
562
778
  return;
563
779
  }
564
780
  await this.deps.giftRepository.update(expired, { transacting });
781
+ await this.deps.giftDeliveryService.cancelPendingForGift(expired.token, { transacting });
565
782
  expiredCount += 1;
566
783
  });
567
784
  }
568
785
  return { expiredCount };
569
786
  }
787
+ async processAbandonedCheckouts() {
788
+ const cutoff = new Date(Date.now() - GIFT_CHECKOUT_RETENTION_DAYS * MS_PER_DAY);
789
+ const deletedCount = await this.deps.giftRepository.deleteAbandonedCheckouts(cutoff);
790
+ return { deletedCount };
791
+ }
570
792
  async processReminders() {
571
793
  const now = new Date();
572
794
  const toRemind = await this.deps.giftRepository.findPendingReminder({
@@ -668,6 +890,9 @@ class GiftService {
668
890
  duration: gift.duration,
669
891
  currency: gift.currency,
670
892
  amount: gift.amount,
893
+ buyer_name: gift.buyerName,
894
+ recipient_name: gift.recipientName,
895
+ message: gift.personalMessage,
671
896
  expires_at: gift.expiresAt,
672
897
  consumes_at: gift.consumesAt,
673
898
  tier: {
@@ -6,6 +6,9 @@ class Gift {
6
6
  token;
7
7
  buyerEmail;
8
8
  buyerMemberId;
9
+ buyerName;
10
+ recipientName;
11
+ personalMessage;
9
12
  redeemerMemberId;
10
13
  tierId;
11
14
  cadence;
@@ -14,6 +17,7 @@ class Gift {
14
17
  amount;
15
18
  stripeCheckoutSessionId;
16
19
  stripePaymentIntentId;
20
+ checkoutStartedAt;
17
21
  consumesAt;
18
22
  expiresAt;
19
23
  status;
@@ -27,6 +31,9 @@ class Gift {
27
31
  this.token = data.token;
28
32
  this.buyerEmail = data.buyerEmail;
29
33
  this.buyerMemberId = data.buyerMemberId;
34
+ this.buyerName = data.buyerName ?? null;
35
+ this.recipientName = data.recipientName ?? null;
36
+ this.personalMessage = data.personalMessage ?? null;
30
37
  this.redeemerMemberId = data.redeemerMemberId;
31
38
  this.tierId = data.tierId;
32
39
  this.cadence = data.cadence;
@@ -35,27 +42,29 @@ class Gift {
35
42
  this.amount = data.amount;
36
43
  this.stripeCheckoutSessionId = data.stripeCheckoutSessionId;
37
44
  this.stripePaymentIntentId = data.stripePaymentIntentId;
45
+ this.checkoutStartedAt = data.checkoutStartedAt ?? null;
38
46
  this.consumesAt = data.consumesAt;
47
+ this.purchasedAt = data.purchasedAt;
39
48
  this.expiresAt = data.expiresAt;
40
49
  this.status = data.status;
41
- this.purchasedAt = data.purchasedAt;
42
50
  this.redeemedAt = data.redeemedAt;
43
51
  this.consumedAt = data.consumedAt;
44
52
  this.expiredAt = data.expiredAt;
45
53
  this.refundedAt = data.refundedAt;
46
- this.consumesSoonReminderSentAt = data.consumesSoonReminderSentAt;
54
+ this.consumesSoonReminderSentAt = data.consumesSoonReminderSentAt ?? null;
47
55
  }
48
56
  static fromPurchase(data) {
49
- const now = new Date();
50
- const expiresAt = new Date(now);
57
+ const purchasedAt = new Date();
58
+ const expiresAt = new Date(purchasedAt);
51
59
  expiresAt.setDate(expiresAt.getDate() + constants_1.GIFT_EXPIRY_DAYS);
52
60
  return new Gift({
53
61
  ...data,
54
62
  redeemerMemberId: null,
55
63
  consumesAt: null,
64
+ checkoutStartedAt: purchasedAt,
56
65
  expiresAt,
57
66
  status: 'purchased',
58
- purchasedAt: now,
67
+ purchasedAt,
59
68
  redeemedAt: null,
60
69
  consumedAt: null,
61
70
  expiredAt: null,
@@ -63,6 +72,48 @@ class Gift {
63
72
  consumesSoonReminderSentAt: null
64
73
  });
65
74
  }
75
+ static fromCheckout(data) {
76
+ return new Gift({
77
+ ...data,
78
+ redeemerMemberId: null,
79
+ stripeCheckoutSessionId: null,
80
+ stripePaymentIntentId: null,
81
+ checkoutStartedAt: new Date(),
82
+ consumesAt: null,
83
+ expiresAt: null,
84
+ status: 'payment_pending',
85
+ purchasedAt: null,
86
+ redeemedAt: null,
87
+ consumedAt: null,
88
+ expiredAt: null,
89
+ refundedAt: null,
90
+ consumesSoonReminderSentAt: null
91
+ });
92
+ }
93
+ completePurchase(data) {
94
+ if (this.status !== 'payment_pending') {
95
+ return null;
96
+ }
97
+ const purchasedAt = data.purchasedAt ?? new Date();
98
+ const expiresAt = new Date(purchasedAt);
99
+ expiresAt.setDate(expiresAt.getDate() + constants_1.GIFT_EXPIRY_DAYS);
100
+ return new Gift({
101
+ ...this,
102
+ ...data,
103
+ purchasedAt,
104
+ expiresAt,
105
+ status: 'purchased'
106
+ });
107
+ }
108
+ bindCheckoutSession(checkoutSessionId) {
109
+ if (this.status !== 'payment_pending' || this.stripeCheckoutSessionId !== null) {
110
+ return null;
111
+ }
112
+ return new Gift({
113
+ ...this,
114
+ stripeCheckoutSessionId: checkoutSessionId
115
+ });
116
+ }
66
117
  isRedeemed() {
67
118
  return this.redeemedAt !== null;
68
119
  }
@@ -76,6 +127,9 @@ class Gift {
76
127
  return this.consumedAt !== null;
77
128
  }
78
129
  checkRedeemable(memberStatus) {
130
+ if (this.status === 'payment_pending') {
131
+ return { redeemable: false, reason: 'payment-pending' };
132
+ }
79
133
  if (this.isRedeemed()) {
80
134
  return { redeemable: false, reason: 'redeemed' };
81
135
  }
@@ -2,16 +2,21 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.service = exports.controller = void 0;
4
4
  exports.init = init;
5
+ exports.recoverPendingDeliveries = recoverPendingDeliveries;
5
6
  const gift_bookshelf_repository_1 = require("./gift-bookshelf-repository");
7
+ const gift_delivery_bookshelf_repository_1 = require("./gift-delivery-bookshelf-repository");
8
+ const gift_delivery_service_1 = require("./gift-delivery-service");
6
9
  const gift_service_1 = require("./gift-service");
7
10
  const gift_reminder_scheduler_1 = require("./gift-reminder-scheduler");
8
11
  const gift_email_service_1 = require("./gift-email-service");
9
12
  const gift_controller_1 = require("./gift-controller");
13
+ const send_gift_delivery_event_1 = require("./events/send-gift-delivery-event");
14
+ let deliveryService;
10
15
  async function init(options) {
11
16
  if (exports.service) {
12
17
  return;
13
18
  }
14
- const { Gift: GiftModel, MemberStripeCustomer: StripeCustomerModel } = require('../../models');
19
+ const { Base: BaseModel, Gift: GiftModel, GiftDelivery: GiftDeliveryModel, MemberStripeCustomer: StripeCustomerModel } = require('../../models');
15
20
  const GiftCheckoutAdapter = require('./gift-checkout-adapter');
16
21
  const membersService = require('../members');
17
22
  const tiersService = require('../tiers');
@@ -23,6 +28,8 @@ async function init(options) {
23
28
  const StartGiftCleanupEvent = require('./events/start-gift-cleanup-event');
24
29
  const jobs = require('./jobs');
25
30
  const { GhostMailer } = require('../mail');
31
+ const MailgunClient = require('../lib/mailgun-client');
32
+ const config = require('../../../shared/config');
26
33
  const settingsCache = require('../../../shared/settings-cache');
27
34
  const labsService = require('../../../shared/labs');
28
35
  const urlUtils = require('../../../shared/url-utils').default;
@@ -31,20 +38,32 @@ async function init(options) {
31
38
  const { blogIcon } = require('../../../server/lib/image');
32
39
  const { t } = require('../i18n');
33
40
  const repository = new gift_bookshelf_repository_1.GiftBookshelfRepository({
34
- GiftModel
41
+ GiftModel,
42
+ knex: BaseModel.knex
43
+ });
44
+ const deliveryRepository = new gift_delivery_bookshelf_repository_1.GiftDeliveryBookshelfRepository({
45
+ GiftDeliveryModel,
46
+ knex: BaseModel.knex
35
47
  });
36
48
  const checkoutAdapter = new GiftCheckoutAdapter({
37
49
  StripeCustomerModel,
38
50
  getStripeApi: () => require('../stripe').api
39
51
  });
40
52
  const giftEmailService = new gift_email_service_1.GiftEmailService({
41
- mailer: new GhostMailer(),
53
+ transactionalMailer: new GhostMailer(),
54
+ bulkMailer: new MailgunClient({ config, settings: settingsCache }),
42
55
  settingsCache,
43
56
  urlUtils,
44
57
  getFromAddress: () => EmailAddressParser.stringify(settingsHelpers.getDefaultEmail()),
45
58
  blogIcon,
46
59
  t
47
60
  });
61
+ const giftDeliveryService = new gift_delivery_service_1.GiftDeliveryService({
62
+ giftRepository: repository,
63
+ giftDeliveryRepository: deliveryRepository,
64
+ tiersService,
65
+ giftEmailService
66
+ });
48
67
  const giftReminderScheduler = new gift_reminder_scheduler_1.GiftReminderScheduler({
49
68
  apiUrl: options.apiUrl,
50
69
  adapter: options.schedulerAdapter,
@@ -53,6 +72,7 @@ async function init(options) {
53
72
  });
54
73
  const giftService = new gift_service_1.GiftService({
55
74
  giftRepository: repository,
75
+ giftDeliveryService,
56
76
  get memberRepository() {
57
77
  return membersService.api.members;
58
78
  },
@@ -67,6 +87,7 @@ async function init(options) {
67
87
  settingsCache
68
88
  });
69
89
  exports.service = giftService;
90
+ deliveryService = giftDeliveryService;
70
91
  exports.controller = new gift_controller_1.GiftController({ service: giftService });
71
92
  DomainEvents.subscribe(SubscriptionActivatedEvent, async (event) => {
72
93
  try {
@@ -86,7 +107,25 @@ async function init(options) {
86
107
  logging.error(err, 'Failed to process gift reminders');
87
108
  }
88
109
  });
110
+ DomainEvents.subscribe(send_gift_delivery_event_1.SendGiftDeliveryEvent, async (event) => {
111
+ const start = Date.now();
112
+ try {
113
+ const result = await giftDeliveryService.send(event.data.deliveryId);
114
+ logging.info(`Gift delivery ${event.data.deliveryId} ${result} in ${Date.now() - start}ms`);
115
+ }
116
+ catch (err) {
117
+ logging.error(err, `Failed to process gift delivery ${event.data.deliveryId}`);
118
+ }
119
+ });
89
120
  DomainEvents.subscribe(StartGiftCleanupEvent, async () => {
121
+ const checkoutStart = Date.now();
122
+ try {
123
+ const { deletedCount } = await giftService.processAbandonedCheckouts();
124
+ logging.info(`Deleted ${deletedCount} abandoned gift checkouts in ${Date.now() - checkoutStart}ms`);
125
+ }
126
+ catch (err) {
127
+ logging.error(err, 'Failed to clean abandoned gift checkouts');
128
+ }
90
129
  const consumedStart = Date.now();
91
130
  try {
92
131
  const { consumedCount, updatedMemberCount } = await giftService.processConsumed();
@@ -103,7 +142,34 @@ async function init(options) {
103
142
  catch (err) {
104
143
  logging.error(err, 'Failed to process expired gifts');
105
144
  }
145
+ try {
146
+ const { sentCount, skippedCount, failedCount } = await giftDeliveryService.recoverPending();
147
+ if (sentCount + skippedCount + failedCount > 0) {
148
+ logging.info(`Gift delivery recovery during cleanup: ${sentCount} sent, ${skippedCount} skipped, ${failedCount} failed`);
149
+ }
150
+ }
151
+ catch (err) {
152
+ logging.error(err, 'Failed to recover pending gift deliveries during cleanup');
153
+ }
106
154
  });
107
155
  jobs.scheduleGiftCleanupJob();
108
156
  jobs.scheduleGiftReminderJob();
109
157
  }
158
+ // Retries deliveries interrupted by a previous shutdown. Sending needs the tiers
159
+ // and members services, which boot alongside this one, so this must run once
160
+ // every service has finished initialising rather than from init().
161
+ async function recoverPendingDeliveries() {
162
+ if (!deliveryService) {
163
+ return;
164
+ }
165
+ const logging = require('@tryghost/logging');
166
+ try {
167
+ const { sentCount, skippedCount, failedCount } = await deliveryService.recoverPending();
168
+ if (sentCount + skippedCount + failedCount > 0) {
169
+ logging.info(`Gift delivery recovery: ${sentCount} sent, ${skippedCount} skipped, ${failedCount} failed`);
170
+ }
171
+ }
172
+ catch (err) {
173
+ logging.error(err, 'Failed to recover pending gift deliveries');
174
+ }
175
+ }
@@ -95,8 +95,11 @@ module.exports = class MailgunClient {
95
95
  messageData['o:testmode'] = true;
96
96
  }
97
97
 
98
- // enable tracking if turned on for this email
99
- if (message.track_opens) {
98
+ if (message.disable_tracking) {
99
+ messageData['o:tracking'] = 'no';
100
+ messageData['o:tracking-clicks'] = 'no';
101
+ messageData['o:tracking-opens'] = 'no';
102
+ } else if (message.track_opens) {
100
103
  messageData['o:tracking-opens'] = true;
101
104
  }
102
105