ghost 6.55.0 → 6.56.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 (280) hide show
  1. package/components/tryghost-custom-field-types-0.0.0.tgz +0 -0
  2. package/components/tryghost-kg-default-cards-10.3.3.tgz +0 -0
  3. package/components/tryghost-kg-default-nodes-2.1.4.tgz +0 -0
  4. package/components/tryghost-kg-default-transforms-1.3.3.tgz +0 -0
  5. package/components/tryghost-kg-html-to-lexical-1.3.3.tgz +0 -0
  6. package/components/tryghost-kg-lexical-html-renderer-1.4.3.tgz +0 -0
  7. package/components/tryghost-nql-string-0.0.0.tgz +0 -0
  8. package/core/built/admin/assets/{Combination-CJG7Flp-.js → Combination-BCm9FL4o.js} +1 -1
  9. package/core/built/admin/assets/{PolarAngleAxis-BGjf7sBu.js → PolarAngleAxis-C4qtHy1B.js} +1 -1
  10. package/core/built/admin/assets/{RadialBarChart-zeFf9mBz.js → RadialBarChart-Bh09Stsj.js} +1 -1
  11. package/core/built/admin/assets/{account-migration-sjHLx5dy.js → account-migration-D3O_yVck.js} +1 -1
  12. package/core/built/admin/assets/activitypub/{account-migration-DoYXtnZ8.js → account-migration-D8FbP-Z-.js} +7 -7
  13. package/core/built/admin/assets/activitypub/activitypub.js +6 -6
  14. package/core/built/admin/assets/activitypub/{avatar-B3DEDB1l.js → avatar-11EA1VSs.js} +2 -2
  15. package/core/built/admin/assets/activitypub/{avatar-flipboard-Cqrg0XMx.js → avatar-flipboard-jfbLG-xe.js} +2 -2
  16. package/core/built/admin/assets/activitypub/{bluesky-sharing-Bxeh6MKW.js → bluesky-sharing-f1jTQe2P.js} +16 -16
  17. package/core/built/admin/assets/activitypub/{content-formatters-BpQARh28.js → content-formatters-nmTOPY9M.js} +3 -3
  18. package/core/built/admin/assets/activitypub/{copy-D9UWIAan.js → copy-BCwo9ep3.js} +2 -2
  19. package/core/built/admin/assets/activitypub/{dist-D8sFfdpC.js → dist-BEfX-0aW.js} +12 -12
  20. package/core/built/admin/assets/activitypub/{domain-CWueFW6q.js → domain-AaDlZlrA.js} +9 -9
  21. package/core/built/admin/assets/activitypub/{edit-profile-BGxPU7N5.js → edit-profile-DPi8Tl5M.js} +945 -945
  22. package/core/built/admin/assets/activitypub/{explore-DBK-u0Ch.js → explore-CxTzjILQ.js} +8 -8
  23. package/core/built/admin/assets/activitypub/{feed-Bcir5RQZ.js → feed-DPv5x2MX.js} +33 -33
  24. package/core/built/admin/assets/activitypub/{field-BV-CIT3d.js → field-DbgA8lat.js} +3 -3
  25. package/core/built/admin/assets/activitypub/{hash-BC9Q0N4c.js → hash-BWuZoYkA.js} +2 -2
  26. package/core/built/admin/assets/activitypub/{inbox-DQ4ZnC1m.js → inbox-35VDQ2K2.js} +2 -2
  27. package/core/built/admin/assets/activitypub/{inbox-yC0nfvzl.js → inbox-DSrF_i1O.js} +85 -85
  28. package/core/built/admin/assets/activitypub/{label-DTRS9TDk.js → label-CPjFBbo7.js} +3 -3
  29. package/core/built/admin/assets/activitypub/layout-CTgrezN7.js +1 -0
  30. package/core/built/admin/assets/activitypub/{moderation-Dwl9t0pb.js → moderation-BkDC1XVZ.js} +11 -11
  31. package/core/built/admin/assets/activitypub/{note-B-zKsZ7r.js → note-xEyH4xT-.js} +7 -7
  32. package/core/built/admin/assets/activitypub/{notifications-DjJZD54t.js → notifications-DClaL3ui.js} +9 -9
  33. package/core/built/admin/assets/activitypub/{preferences-CoU8vs6d.js → preferences-DpnOa-4Z.js} +20 -20
  34. package/core/built/admin/assets/activitypub/{profile-BAsaJekx.js → profile-Dvi9mz-6.js} +237 -237
  35. package/core/built/admin/assets/activitypub/{reply-CpdMeAO5.js → reply-Cs9ySd6Z.js} +2 -2
  36. package/core/built/admin/assets/activitypub/{routes-CLH3OVTP.js → routes-DFBeTnfn.js} +1139 -1150
  37. package/core/built/admin/assets/activitypub/{separator-BI1FLsYM.js → separator-C5u_VjzY.js} +3 -3
  38. package/core/built/admin/assets/activitypub/{settings-DA6pNJ-Q.js → settings-CV2fFu6D.js} +13 -13
  39. package/core/built/admin/assets/activitypub/{site-CQD42Pw5.js → site-_TgDd_EM.js} +81 -81
  40. package/core/built/admin/assets/activitypub/{step-1-DIDg4aHb.js → step-1-BfnNL2oF.js} +5 -5
  41. package/core/built/admin/assets/activitypub/{step-2-BfGe8BYt.js → step-2-DLndZkO-.js} +6 -6
  42. package/core/built/admin/assets/activitypub/{step-3-mE9spAqw.js → step-3-A-PZTtRE.js} +10 -10
  43. package/core/built/admin/assets/activitypub/{tabs-Cg0saow1.js → tabs-BUbz7tea.js} +317 -315
  44. package/core/built/admin/assets/activitypub/{topic-filter-DLHBZT93.js → topic-filter-BdX_xYYT.js} +3 -3
  45. package/core/built/admin/assets/activitypub/{use-activity-pub-queries-VzbN99u_.js → use-activity-pub-queries-EwuRemHh.js} +2 -2
  46. package/core/built/admin/assets/activitypub/{use-navigate-with-base-path-CAwXTmqH.js → use-navigate-with-base-path-cy4ACBc4.js} +92 -90
  47. package/core/built/admin/assets/activitypub/{use-reply-chain-data-Br4aOWOh.js → use-reply-chain-data-CyFcGfan.js} +5 -5
  48. package/core/built/admin/assets/activitypub/{x-C8tAnXly.js → x-7zk8mV2p.js} +2 -2
  49. package/core/built/admin/assets/{audience-DjEWNhp3.js → audience-CkBb2LlO.js} +1 -1
  50. package/core/built/admin/assets/{automated-emails-DRy7qY-J.js → automated-emails-6t2bIRWf.js} +1 -1
  51. package/core/built/admin/assets/{automations-Bx3VcFIL.js → automations-cMllU545.js} +1 -1
  52. package/core/built/admin/assets/{bluesky-sharing-ClMd0-Ft.js → bluesky-sharing-DPGpyD-G.js} +1 -1
  53. package/core/built/admin/assets/{box-BWbhXh21.js → box-By_EgGtm.js} +1 -1
  54. package/core/built/admin/assets/{breadcrumb-BD0U2ygg.js → breadcrumb-DIfcF5ow.js} +1 -1
  55. package/core/built/admin/assets/build-CsiH26uS.js +1 -0
  56. package/core/built/admin/assets/{card-DGoDJzrn.js → card-aUHX0hyR.js} +1 -1
  57. package/core/built/admin/assets/{chart-BuRT2Y6p.js → chart-BnXyfH11.js} +1 -1
  58. package/core/built/admin/assets/{chart-helpers-fjQKRVyY.js → chart-helpers-DV0FkdcD.js} +1 -1
  59. package/core/built/admin/assets/{checkbox-CWa28lgO.js → checkbox-BIl077a0.js} +1 -1
  60. package/core/built/admin/assets/{chunk.481.6c7395a49484bb4583aa.js → chunk.332.05991a101d67f732dec3.js} +43 -42
  61. package/core/built/admin/assets/chunk.524.87b52f4fada529df2c4c.js +25 -0
  62. package/core/built/admin/assets/chunk.582.340bcca873e806cbd4bb.js +27 -0
  63. package/core/built/admin/assets/{code-editor-view-CCr0rBWS.js → code-editor-view-C0aWNwdl.js} +1 -1
  64. package/core/built/admin/assets/comments-qwhIjH3c.js +1 -0
  65. package/core/built/admin/assets/{container-BYz2Ed39.js → container-LiZf3XdY.js} +1 -1
  66. package/core/built/admin/assets/{data-list-16rkJYYi.js → data-list-CupF7bjM.js} +1 -1
  67. package/core/built/admin/assets/{date-range-select-BFp58EMk.js → date-range-select-dXc-GsVe.js} +1 -1
  68. package/core/built/admin/assets/{disabled-sources-indicator-CUxMfiPa.js → disabled-sources-indicator-DYAXW-6V.js} +1 -1
  69. package/core/built/admin/assets/dist-BUKmD51u.js +1 -0
  70. package/core/built/admin/assets/{dist-2Tnz2Yfq.js → dist-CNlMj7ML.js} +1 -1
  71. package/core/built/admin/assets/{dist-DGzPJ_bO.js → dist-Cb5RSHSg.js} +1 -1
  72. package/core/built/admin/assets/{dist-P_ohUlTi.js → dist-CqO2lWS0.js} +1 -1
  73. package/core/built/admin/assets/domain-B5ogXUBa.js +1 -0
  74. package/core/built/admin/assets/{dropzone-DlK-K2DQ.js → dropzone-BnFG7dDP.js} +1 -1
  75. package/core/built/admin/assets/{edit-profile-BKT-REJw.js → edit-profile-B07jCZy9.js} +1 -1
  76. package/core/built/admin/assets/editor-DDYSOXGN.js +8 -0
  77. package/core/built/admin/assets/{empty-indicator-oX1u1Pkn.js → empty-indicator-WB1WgT9h.js} +1 -1
  78. package/core/built/admin/assets/{explore-ClUpJZOp.js → explore-Ca1y8HW_.js} +1 -1
  79. package/core/built/admin/assets/{feed-DF92dUMP.js → feed-x4iFMBP4.js} +1 -1
  80. package/core/built/admin/assets/{field-Do7lieBA.js → field-ChCAabZg.js} +1 -1
  81. package/core/built/admin/assets/filter-sources-LdtH1e-T.js +1 -0
  82. package/core/built/admin/assets/filters-CLehPo_D.js +1 -0
  83. package/core/built/admin/assets/filters-CjVKRP8U.js +1 -0
  84. package/core/built/admin/assets/{ghost-c4057474943effc568c5659ccc4909d7.js → ghost-7f01131115186d1af84ead76e79646dd.js} +149 -129
  85. package/core/built/admin/assets/{ghost-a9c788f6f0b1ea6cf0f14ca4b13f4496.css → ghost-d38c30b866e59f2e06a357b5eb844fe5.css} +1 -1
  86. package/core/built/admin/assets/{ghost-dark-db36d795d21c61426837df40af270966.css → ghost-dark-1bffd5099ba7a749303133efacf8480b.css} +1 -1
  87. package/core/built/admin/assets/{gift-link-modal-f9VIMNDv.js → gift-link-modal-Buqh2vXh.js} +1 -1
  88. package/core/built/admin/assets/{growth-B3I6gNLm.js → growth-DajOBdN0.js} +1 -1
  89. package/core/built/admin/assets/{growth-CVw1My4S.js → growth-Dy2UOSay.js} +2 -2
  90. package/core/built/admin/assets/{inbox-CFBSu4Ed.js → inbox-BUOLDoji.js} +12 -12
  91. package/core/built/admin/assets/index-08n05-rc.js +67 -0
  92. package/core/built/admin/assets/index-DgUzgFvS.css +1 -0
  93. package/core/built/admin/assets/{inline-EMAm3okk.js → inline-Cw-gigDW.js} +1 -1
  94. package/core/built/admin/assets/input-group-uSK3XT7b.js +1 -0
  95. package/core/built/admin/assets/{input-surface-BpAjZXY5.js → input-surface-BdOe4KzO.js} +1 -1
  96. package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +9535 -9527
  97. package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +124 -124
  98. package/core/built/admin/assets/{koenig-lexical-f8vHRYvz.js → koenig-lexical-BFc4yFed.js} +86 -86
  99. package/core/built/admin/assets/{kpi-card-DSap9rAy.js → kpi-card-BN-E6WYX.js} +1 -1
  100. package/core/built/admin/assets/{kpi-card-DU0rn9Cj.js → kpi-card-CCt-qeWD.js} +1 -1
  101. package/core/built/admin/assets/{kpi-helpers-Dz33AV6h.js → kpi-helpers-DlxTbTs4.js} +1 -1
  102. package/core/built/admin/assets/{kpi-tabs-BtTTrKvJ.js → kpi-tabs-BlcUttpn.js} +1 -1
  103. package/core/built/admin/assets/{label-DS8Mi6G5.js → label-AVrxwmsz.js} +1 -1
  104. package/core/built/admin/assets/{label-picker-1DvSuLA1.js → label-picker-DVnpu_IF.js} +1 -1
  105. package/core/built/admin/assets/labels-DAIqKed7.js +1 -0
  106. package/core/built/admin/assets/layout-B0YLh7R9.js +1 -0
  107. package/core/built/admin/assets/link-helpers-D9LsDOkG.js +1 -0
  108. package/core/built/admin/assets/{list-page-B6ANtzkd.js → list-page-CdsnQR9N.js} +1 -1
  109. package/core/built/admin/assets/member-custom-fields-C8NpZYza.js +1 -0
  110. package/core/built/admin/assets/member-detail-CIVAdKXf.js +1 -0
  111. package/core/built/admin/assets/{members-CQpVFJu4.js → members-oLPiSkXD.js} +2 -2
  112. package/core/built/admin/assets/modals-CB7v-k3T.js +31 -0
  113. package/core/built/admin/assets/{moderation-DySbgyUS.js → moderation-CQ3urTFE.js} +1 -1
  114. package/core/built/admin/assets/newsletter-rbeDX9Wk.js +1 -0
  115. package/core/built/admin/assets/newsletters-vt3Ni1Ov.js +1 -0
  116. package/core/built/admin/assets/{note-C8kxrz6w.js → note-Dk877BTv.js} +1 -1
  117. package/core/built/admin/assets/{notifications-CgtSfX7t.js → notifications-CPlLjPxO.js} +1 -1
  118. package/core/built/admin/assets/{onboarding-route-4qtziA9d.js → onboarding-route-DdStOtjy.js} +1 -1
  119. package/core/built/admin/assets/{overview-Bf2sbznq.js → overview--4V-kx9c.js} +1 -1
  120. package/core/built/admin/assets/overview-DSLv30oz.js +1 -0
  121. package/core/built/admin/assets/{page-header-B3Wb9Jqv.js → page-header-pp4dWmOS.js} +1 -1
  122. package/core/built/admin/assets/{post-analytics-We-ko5Cb.js → post-analytics-BMJrI81R.js} +1 -1
  123. package/core/built/admin/assets/post-analytics-header-pU2m9ZuV.js +1 -0
  124. package/core/built/admin/assets/{post-analytics-provider-B6UynTGb.js → post-analytics-provider-Btml5vJI.js} +1 -1
  125. package/core/built/admin/assets/{post-share-modal-DFfO93_0.js → post-share-modal-AcUZkbfL.js} +1 -1
  126. package/core/built/admin/assets/preferences-Ru4CRPHC.js +5 -0
  127. package/core/built/admin/assets/profile-BNb1oj7D.js +1 -0
  128. package/core/built/admin/assets/{router-provider-C1R4AZ2o.js → router-provider-BmEZf6c6.js} +1 -1
  129. package/core/built/admin/assets/select-B0HteHrp.js +1 -0
  130. package/core/built/admin/assets/settings-B1C9Q3wo.js +4 -0
  131. package/core/built/admin/assets/{settings-WXNeWyU1.js → settings-BGgk7oCI.js} +1 -1
  132. package/core/built/admin/assets/{shade-app-B3heOAfz-CGXQwWeO.js → shade-app-B3heOAfz-BhQXxNkT.js} +1 -1
  133. package/core/built/admin/assets/{share-modal-DsgW3wbg.js → share-modal-BeuydaBf.js} +1 -1
  134. package/core/built/admin/assets/social-urls-DA2lXrhW.js +8 -0
  135. package/core/built/admin/assets/{sort-button-Bb_FCl7e.js → sort-button-Df4t2AL8.js} +1 -1
  136. package/core/built/admin/assets/{source-icon-BeaRxUn9.js → source-icon-ajoaKb41.js} +1 -1
  137. package/core/built/admin/assets/{stack-xiN0eQ6Z.js → stack-CIfOAAVf.js} +1 -1
  138. package/core/built/admin/assets/{stats-Bjj_kmJ1.js → stats-C_lfxZAw.js} +1 -1
  139. package/core/built/admin/assets/{stats-view-BxPbW6K1.js → stats-view-Cxot1iD8.js} +1 -1
  140. package/core/built/admin/assets/{step-1-B1e3-1F8.js → step-1-ogAA4lDr.js} +1 -1
  141. package/core/built/admin/assets/{step-2-C5WaeBJo.js → step-2-C5Xini-7.js} +1 -1
  142. package/core/built/admin/assets/{step-3-B46iVPwu.js → step-3-CZv1FV81.js} +2 -2
  143. package/core/built/admin/assets/{switch-z5Y6Uzt6.js → switch-CyQcwMAv.js} +1 -1
  144. package/core/built/admin/assets/{table-CB-4_HBJ.js → table-DFj1wiYv.js} +1 -1
  145. package/core/built/admin/assets/{tabs-CCEgKYzq.js → tabs-D4y1gqUd.js} +1 -1
  146. package/core/built/admin/assets/{tags-Cg2VJGmG.js → tags-Dmft-Y4O.js} +1 -1
  147. package/core/built/admin/assets/{text-DwtjwGkh.js → text-DU9Hv33_.js} +1 -1
  148. package/core/built/admin/assets/textarea-d9qEkMOP.js +1 -0
  149. package/core/built/admin/assets/{toggle-group-b3IlxvhV.js → toggle-group-R2QMFvXW.js} +1 -1
  150. package/core/built/admin/assets/{topic-filter-DAFT87tQ.js → topic-filter-DGDxDalo.js} +1 -1
  151. package/core/built/admin/assets/{use-analytics-data-S0FTd43s.js → use-analytics-data-C76v4LIp.js} +1 -1
  152. package/core/built/admin/assets/use-filter-params-WrdhAISv.js +1 -0
  153. package/core/built/admin/assets/{use-growth-stats-CIaiHOVb.js → use-growth-stats-aNXJOmQr.js} +1 -1
  154. package/core/built/admin/assets/{use-post-referrers-Cq2vBPrA.js → use-post-referrers-CG0SZioj.js} +1 -1
  155. package/core/built/admin/assets/{use-reply-chain-data-DBDRtGu-.js → use-reply-chain-data-RLdPKRmS.js} +1 -1
  156. package/core/built/admin/assets/{use-simple-pagination-CjYNxdwe.js → use-simple-pagination-DgcF69KI.js} +1 -1
  157. package/core/built/admin/assets/{use-tinybird-query-falHAtuK.js → use-tinybird-query-C1R_Kbmy.js} +1 -1
  158. package/core/built/admin/assets/{use-tinybird-token-D7-8ND1y.js → use-tinybird-token-C9fPjVM_.js} +1 -1
  159. package/core/built/admin/assets/{virtual-list-CvnC9zF2.js → virtual-list-BmTUzeoF.js} +1 -1
  160. package/core/built/admin/assets/{web-B_5GZRvH.js → web-BqW0b0_C.js} +1 -1
  161. package/core/built/admin/assets/web-Cy5GKMWM.js +2 -0
  162. package/core/built/admin/index.html +16 -16
  163. package/core/frontend/services/data/fetch-data.js +10 -5
  164. package/core/frontend/services/routing/api-adapter.js +115 -0
  165. package/core/frontend/services/routing/api-adapter.ts +157 -0
  166. package/core/frontend/services/routing/collection-router.js +5 -3
  167. package/core/frontend/services/routing/controllers/index.js +1 -1
  168. package/core/frontend/services/routing/controllers/rss.js +26 -31
  169. package/core/frontend/services/routing/controllers/rss.ts +66 -0
  170. package/core/frontend/services/routing/controllers/static.js +8 -7
  171. package/core/frontend/services/routing/parent-router.js +17 -4
  172. package/core/frontend/services/routing/permalink-adapter.js +26 -0
  173. package/core/frontend/services/routing/permalink-adapter.ts +23 -0
  174. package/core/frontend/services/routing/registry.js +59 -33
  175. package/core/frontend/services/routing/router-manager.js +9 -13
  176. package/core/frontend/services/routing/static-routes-router.js +4 -4
  177. package/core/frontend/services/routing/taxonomy-router.js +6 -2
  178. package/core/server/data/migrations/versions/6.56/2026-08-03-19-27-51-add-automation-run-step-id-to-automated-email-recipients.js +8 -0
  179. package/core/server/data/migrations/versions/6.56/2026-08-04-14-42-39-restrict-notification-add-permission-to-administrators.js +12 -0
  180. package/core/server/data/schema/fixtures/fixtures.json +2 -2
  181. package/core/server/data/schema/schema.js +1 -0
  182. package/core/server/models/post.js +10 -1
  183. package/core/server/services/automations/automations-repository.ts +3 -1
  184. package/core/server/services/automations/database-automations-repository.js +33 -14
  185. package/core/server/services/automations/database-automations-repository.ts +41 -15
  186. package/core/server/services/automations/get-scheduler-idempotency-key.js +13 -0
  187. package/core/server/services/automations/get-scheduler-idempotency-key.ts +8 -0
  188. package/core/server/services/automations/poll.js +3 -1
  189. package/core/server/services/automations/poll.ts +4 -1
  190. package/core/server/services/automations/service.js +5 -1
  191. package/core/server/services/automations/service.ts +5 -1
  192. package/core/server/services/gift-links/CONTEXT.md +9 -0
  193. package/core/server/services/gifts/CONTEXT.md +55 -0
  194. package/core/server/services/gifts/README.md +28 -0
  195. package/core/server/services/gifts/gift-bookshelf-repository.js +65 -45
  196. package/core/server/services/gifts/gift-bookshelf-repository.ts +203 -74
  197. package/core/server/services/gifts/gift-checkout-adapter.js +59 -0
  198. package/core/server/services/gifts/gift-checkout-offer.js +74 -0
  199. package/core/server/services/gifts/gift-checkout-offer.ts +127 -0
  200. package/core/server/services/gifts/gift-codec.js +63 -0
  201. package/core/server/services/gifts/gift-codec.ts +62 -0
  202. package/core/server/services/gifts/gift-controller.js +6 -31
  203. package/core/server/services/gifts/gift-controller.ts +6 -69
  204. package/core/server/services/gifts/gift-email-service.ts +3 -2
  205. package/core/server/services/gifts/gift-schema.js +34 -0
  206. package/core/server/services/gifts/gift-schema.ts +39 -0
  207. package/core/server/services/gifts/gift-service-wrapper.js +13 -11
  208. package/core/server/services/gifts/gift-service.js +265 -52
  209. package/core/server/services/gifts/gift-service.ts +431 -97
  210. package/core/server/services/gifts/gift.ts +16 -39
  211. package/core/server/services/link-tracking/link-click-repository.js +6 -1
  212. package/core/server/services/link-tracking/link-click-tracking-service.js +12 -3
  213. package/core/server/services/member-welcome-emails/member-welcome-email-renderer.js +4 -3
  214. package/core/server/services/member-welcome-emails/service.js +7 -3
  215. package/core/server/services/members/api.js +1 -2
  216. package/core/server/services/members/import-export/import/importer.ts +3 -2
  217. package/core/server/services/members/import-export/index.js +10 -2
  218. package/core/server/services/members/import-export/index.ts +14 -4
  219. package/core/server/services/members/members-api/controllers/router-controller.js +38 -36
  220. package/core/server/services/members/members-api/members-api.js +13 -7
  221. package/core/server/services/members/members-api/repositories/event-repository.js +4 -94
  222. package/core/server/services/members/members-api/services/member-bread-service.js +3 -3
  223. package/core/server/services/members/members-api/services/payments-service.js +6 -68
  224. package/core/server/services/members-custom-fields/schema.js +8 -1
  225. package/core/server/services/members-custom-fields/schema.ts +8 -0
  226. package/core/server/services/members-custom-fields/values-service.js +118 -2
  227. package/core/server/services/members-custom-fields/values-service.ts +134 -4
  228. package/core/server/services/oembed/oembed-service.js +44 -3
  229. package/core/server/services/posts/posts-service.js +11 -0
  230. package/core/server/services/route-settings/dynamic-routing-service.js +1 -3
  231. package/core/server/services/route-settings/route-settings-parser.js +25 -25
  232. package/core/server/services/route-settings/route-settings-parser.ts +27 -27
  233. package/core/server/services/route-settings/validation-errors.js +68 -4
  234. package/core/server/services/route-settings/validation-errors.ts +79 -4
  235. package/core/server/services/stats/posts-stats-service.js +27 -23
  236. package/core/server/services/stripe/services/webhook/charge-refunded-event-service.js +1 -1
  237. package/core/server/services/stripe/services/webhook/checkout-session-event-service.js +16 -4
  238. package/core/server/services/stripe/stripe-api.js +10 -0
  239. package/core/server/web/gift-preview/controller.js +10 -25
  240. package/core/shared/labs.js +2 -2
  241. package/package.json +10 -9
  242. package/pnpm-lock.yaml +28 -18
  243. package/pnpm-workspace.yaml +5 -4
  244. package/components/tryghost-kg-default-nodes-2.1.3.tgz +0 -0
  245. package/core/built/admin/assets/activitypub/layout-qx-bUL1k.js +0 -1
  246. package/core/built/admin/assets/chunk.524.cffd399783a519e404a9.js +0 -25
  247. package/core/built/admin/assets/chunk.582.eebf0d0b2d202bb746a5.js +0 -27
  248. package/core/built/admin/assets/comments-DyUSMUdj.js +0 -1
  249. package/core/built/admin/assets/dist-CjY-f5Z1.js +0 -1
  250. package/core/built/admin/assets/domain-C0bV2UV8.js +0 -1
  251. package/core/built/admin/assets/editor-Dp865O-4.js +0 -8
  252. package/core/built/admin/assets/filter-sources-BV2rnpfA.js +0 -1
  253. package/core/built/admin/assets/filters-CUXJ2Nm9.js +0 -1
  254. package/core/built/admin/assets/filters-DfbIWc_l.js +0 -1
  255. package/core/built/admin/assets/index-9Cw8z3rA.css +0 -1
  256. package/core/built/admin/assets/index-BGG7NckO.js +0 -67
  257. package/core/built/admin/assets/input-group-BSgG_lVw.js +0 -1
  258. package/core/built/admin/assets/labels-CDSUt4J_.js +0 -1
  259. package/core/built/admin/assets/layout-yQEi2qDM.js +0 -1
  260. package/core/built/admin/assets/link-helpers-7EYqvvHs.js +0 -1
  261. package/core/built/admin/assets/member-custom-fields-yOMHyAg9.js +0 -1
  262. package/core/built/admin/assets/member-detail-BAq7gDaa.js +0 -1
  263. package/core/built/admin/assets/modals-ReORvWnu.js +0 -31
  264. package/core/built/admin/assets/newsletter-CJQvtHqN.js +0 -1
  265. package/core/built/admin/assets/newsletters-B3QQXfGT.js +0 -1
  266. package/core/built/admin/assets/nql-BsgFMd93.js +0 -1
  267. package/core/built/admin/assets/overview-CBfePxcr.js +0 -1
  268. package/core/built/admin/assets/post-analytics-header-Bmc9su6q.js +0 -1
  269. package/core/built/admin/assets/preferences-DJPfHEGu.js +0 -5
  270. package/core/built/admin/assets/profile-DqBQp-gT.js +0 -1
  271. package/core/built/admin/assets/select-C5_xEt5b.js +0 -1
  272. package/core/built/admin/assets/settings-BwiVIXau.js +0 -4
  273. package/core/built/admin/assets/social-urls-BQPFEk1_.js +0 -8
  274. package/core/built/admin/assets/textarea-Bjccvh_E.js +0 -1
  275. package/core/built/admin/assets/use-filter-params-Gpoq2E1L.js +0 -1
  276. package/core/built/admin/assets/web-jFYWCKDn.js +0 -2
  277. package/core/server/services/gifts/gift-repository.js +0 -2
  278. package/core/server/services/gifts/gift-repository.ts +0 -29
  279. package/core/server/services/route-settings/activation-bridge.js +0 -174
  280. package/core/server/services/route-settings/activation-bridge.ts +0 -250
@@ -1,11 +1,20 @@
1
1
  import crypto from 'node:crypto';
2
2
  import errors from '@tryghost/errors';
3
3
  import logging from '@tryghost/logging';
4
+ import type {Knex} from 'knex';
5
+ import {z} from 'zod';
4
6
  import {Gift} from './gift';
5
- import type {GiftRepository} from './gift-repository';
7
+ import type {GiftEventBrowseOptions, GiftEventPage, GiftRepository} from './gift-bookshelf-repository';
6
8
  import type {GiftReminderScheduler} from './gift-reminder-scheduler';
9
+ import {GiftCadenceSchema, type GiftCadence} from './gift-schema';
7
10
  import tpl from '@tryghost/tpl';
8
11
  import {GIFT_REMINDER_FLOOR_DAYS, GIFT_REMINDER_LEAD_DAYS} from './constants';
12
+ import {
13
+ resolveGiftDuration,
14
+ validateGiftCheckoutOffer,
15
+ type GiftCheckoutTier,
16
+ type ResolvedGiftDuration
17
+ } from './gift-checkout-offer';
9
18
 
10
19
  const MS_PER_DAY = 24 * 60 * 60 * 1000;
11
20
  const GIFT_REMINDER_LEAD_MS = GIFT_REMINDER_LEAD_DAYS * MS_PER_DAY;
@@ -45,12 +54,14 @@ interface MemberRepository {
45
54
  ): Promise<unknown>;
46
55
  }
47
56
 
48
- type Tier = {
57
+ type Tier = Omit<GiftCheckoutTier, 'currency'> & {
58
+ id: string | {
59
+ toHexString(): string;
60
+ };
49
61
  name: string;
50
- currency: string | null;
51
- monthlyPrice: number | null;
52
- yearlyPrice: number | null;
53
- toJSON?: () => {
62
+ currency: string;
63
+ getPrice(cadence: GiftCadence): number;
64
+ toJSON(): {
54
65
  id: string;
55
66
  name: string;
56
67
  description: string | null;
@@ -69,7 +80,7 @@ interface GiftEmailService {
69
80
  buyerEmail: string;
70
81
  token: string;
71
82
  tierName: string;
72
- cadence: 'month' | 'year';
83
+ cadence: GiftCadence;
73
84
  duration: number;
74
85
  expiresAt: Date;
75
86
  }): Promise<void>;
@@ -89,7 +100,7 @@ interface StaffServiceEmails {
89
100
  amount: number;
90
101
  currency: string;
91
102
  tierName: string;
92
- cadence: 'month' | 'year';
103
+ cadence: GiftCadence;
93
104
  duration: number;
94
105
  }): Promise<void>;
95
106
  notifyGiftSubscriptionStarted(data: {
@@ -97,24 +108,26 @@ interface StaffServiceEmails {
97
108
  memberEmail: string;
98
109
  memberName: string | null;
99
110
  tierName: string;
100
- cadence: 'month' | 'year';
111
+ cadence: GiftCadence;
101
112
  duration: number;
102
113
  buyerEmail: string;
103
114
  }): Promise<void>;
104
115
  }
105
116
 
106
- export interface GiftPurchaseData {
107
- token: string;
108
- buyerEmail: string;
109
- stripeCustomerId: string | null;
110
- tierId: string;
111
- cadence: 'month' | 'year';
112
- duration: string;
113
- currency: string;
114
- amount: number;
115
- stripeCheckoutSessionId: string;
116
- stripePaymentIntentId: string;
117
- }
117
+ const GiftPurchaseDataSchema = z.object({
118
+ token: z.string().min(1),
119
+ buyerEmail: z.string().min(1),
120
+ stripeCustomerId: z.string().min(1).nullable(),
121
+ tierId: z.string().min(1),
122
+ cadence: GiftCadenceSchema,
123
+ duration: z.number().int().positive(),
124
+ currency: z.string().min(1),
125
+ amount: z.number().int().nonnegative(),
126
+ stripeCheckoutSessionId: z.string().min(1),
127
+ stripePaymentIntentId: z.string().min(1)
128
+ });
129
+
130
+ export type GiftPurchaseData = z.infer<typeof GiftPurchaseDataSchema>;
118
131
 
119
132
  interface GiftServiceDeps {
120
133
  giftRepository: GiftRepository;
@@ -123,6 +136,16 @@ interface GiftServiceDeps {
123
136
  giftEmailService: GiftEmailService;
124
137
  staffServiceEmails: StaffServiceEmails;
125
138
  giftReminderScheduler: Pick<GiftReminderScheduler, 'scheduleFor'>;
139
+ checkoutAdapter: {
140
+ getCustomerId(buyer: GiftCheckoutBuyer): Promise<string | null>;
141
+ createSession(data: GiftCheckoutSession): Promise<string>;
142
+ };
143
+ labsService: {
144
+ isSet(flag: string): boolean;
145
+ };
146
+ settingsCache: {
147
+ get(key: string): unknown;
148
+ };
126
149
  }
127
150
 
128
151
  interface ReminderSend {
@@ -131,6 +154,74 @@ interface ReminderSend {
131
154
  consumesAt: Date;
132
155
  }
133
156
 
157
+ export interface GiftCheckoutBuyer {
158
+ memberId: string | null;
159
+ email: string | null;
160
+ name: string | null;
161
+ isAuthenticated: boolean;
162
+ }
163
+
164
+ export interface StartGiftCheckoutInput {
165
+ tierId?: string;
166
+ offerId?: string;
167
+ cadence?: string;
168
+ duration?: number;
169
+ metadata: Record<string, unknown>;
170
+ successUrl: string;
171
+ cancelUrl?: string;
172
+ buyer: GiftCheckoutBuyer;
173
+ }
174
+
175
+ interface GiftCheckoutSession {
176
+ amount: number;
177
+ currency: string;
178
+ tierName: string;
179
+ cadence: GiftCadence;
180
+ duration: number;
181
+ metadata: Record<string, unknown>;
182
+ successUrl: string;
183
+ cancelUrl?: string;
184
+ customerId: string | null;
185
+ customerEmail: string | null;
186
+ }
187
+
188
+ export interface GiftRedemption {
189
+ token: string;
190
+ cadence: GiftCadence;
191
+ duration: number;
192
+ currency: string;
193
+ amount: number;
194
+ expires_at: Date;
195
+ consumes_at: Date | null;
196
+ tier: {
197
+ id: string;
198
+ name: string;
199
+ description: string | null;
200
+ benefits: string[];
201
+ };
202
+ }
203
+
204
+ export interface GiftContinuation {
205
+ tierId: string;
206
+ cadence: GiftCadence;
207
+ trialDays: number | null;
208
+ }
209
+
210
+ export interface GiftMemberPresentation {
211
+ cadence: GiftCadence;
212
+ currency: string;
213
+ amount: number;
214
+ }
215
+
216
+ export interface GiftPreview {
217
+ cadence: GiftCadence;
218
+ duration: number;
219
+ tier: {
220
+ id: string;
221
+ name: string;
222
+ };
223
+ }
224
+
134
225
  export class GiftService {
135
226
  private readonly deps: GiftServiceDeps;
136
227
 
@@ -138,7 +229,118 @@ export class GiftService {
138
229
  this.deps = deps;
139
230
  }
140
231
 
141
- generateToken(): string {
232
+ async startCheckout(input: StartGiftCheckoutInput): Promise<{url: string}> {
233
+ if (input.offerId) {
234
+ throw new errors.BadRequestError({
235
+ message: 'Bad Request.',
236
+ context: 'Offers cannot be applied to gift subscriptions'
237
+ });
238
+ }
239
+
240
+ if (!input.tierId) {
241
+ throw new errors.BadRequestError({
242
+ message: 'Bad Request.',
243
+ context: 'Expected offerId or tierId, received none'
244
+ });
245
+ }
246
+
247
+ let resolvedDuration: ResolvedGiftDuration | null = null;
248
+ let cadence: GiftCadence;
249
+
250
+ if (this.deps.labsService.isSet('giftSubCustomization')) {
251
+ resolvedDuration = resolveGiftDuration(input);
252
+ cadence = resolvedDuration.cadence;
253
+ } else {
254
+ if (input.cadence !== 'month' && input.cadence !== 'year') {
255
+ const receivedCadence = input.cadence ? `"${input.cadence}"` : input.cadence;
256
+
257
+ throw new errors.BadRequestError({
258
+ message: 'Bad Request.',
259
+ context: `Expected cadence to be "month" or "year", received ${receivedCadence}`
260
+ });
261
+ }
262
+
263
+ cadence = input.cadence;
264
+ }
265
+
266
+ let tier: Tier | null;
267
+ try {
268
+ tier = await this.deps.tiersService.api.read(input.tierId);
269
+ } catch (err) {
270
+ logging.error(err);
271
+ tier = null;
272
+ }
273
+
274
+ if (!tier) {
275
+ throw new errors.BadRequestError({
276
+ message: 'This tier does not exist.',
277
+ context: `Tier with id "${input.tierId}" not found`
278
+ });
279
+ }
280
+
281
+ if (tier.status === 'archived') {
282
+ throw new errors.NoPermissionError({
283
+ message: 'This tier is archived.'
284
+ });
285
+ }
286
+
287
+ let duration = 1;
288
+ let totalMonths: number | undefined;
289
+ let amount = tier.getPrice(cadence);
290
+
291
+ if (resolvedDuration) {
292
+ const plan = validateGiftCheckoutOffer({
293
+ tier,
294
+ portalPlans: this.deps.settingsCache.get('portal_plans'),
295
+ offer: resolvedDuration
296
+ });
297
+
298
+ cadence = plan.cadence;
299
+ duration = plan.duration;
300
+ totalMonths = plan.totalMonths;
301
+ amount = plan.amount;
302
+ }
303
+
304
+ const tierId = typeof tier.id === 'string' ? tier.id : tier.id.toHexString();
305
+ const token = this.generateToken();
306
+ const successUrl = new URL(input.successUrl);
307
+
308
+ successUrl.searchParams.set('stripe', 'gift-purchase-success');
309
+ successUrl.searchParams.set('gift_token', token);
310
+ successUrl.searchParams.set('gift_tier', tierId);
311
+ successUrl.searchParams.set('gift_cadence', cadence);
312
+ if (totalMonths !== undefined) {
313
+ successUrl.searchParams.set('gift_duration', String(totalMonths));
314
+ }
315
+
316
+ const customerId = input.buyer.isAuthenticated
317
+ ? await this.deps.checkoutAdapter.getCustomerId(input.buyer)
318
+ : null;
319
+
320
+ const url = await this.deps.checkoutAdapter.createSession({
321
+ amount,
322
+ currency: tier.currency.toLowerCase(),
323
+ tierName: tier.name,
324
+ cadence,
325
+ duration,
326
+ metadata: {
327
+ ...input.metadata,
328
+ ghost_gift: 'true',
329
+ gift_token: token,
330
+ tier_id: tierId,
331
+ cadence,
332
+ duration: String(duration)
333
+ },
334
+ successUrl: successUrl.toString(),
335
+ cancelUrl: input.cancelUrl,
336
+ customerId,
337
+ customerEmail: customerId ? null : input.buyer.email
338
+ });
339
+
340
+ return {url};
341
+ }
342
+
343
+ private generateToken(): string {
142
344
  /**
143
345
  * Combinations: 62^12 ≈ 3.23 × 10^21 (~3.23 sextillion)
144
346
  * Entropy: 12 × log2(62) ≈ 71.45 bits
@@ -153,12 +355,17 @@ export class GiftService {
153
355
  return token;
154
356
  }
155
357
 
156
- async recordPurchase(data: GiftPurchaseData): Promise<boolean> {
157
- const duration = Number.parseInt(data.duration);
158
-
159
- if (Number.isNaN(duration)) {
160
- throw new errors.ValidationError({message: `Invalid gift duration: ${data.duration}`});
358
+ async completePurchase(input: GiftPurchaseData): Promise<boolean> {
359
+ const parsed = GiftPurchaseDataSchema.safeParse(input);
360
+ if (!parsed.success) {
361
+ const issue = parsed.error.issues[0];
362
+ throw new errors.ValidationError({
363
+ message: 'Invalid gift purchase data.',
364
+ property: issue.path.join('.'),
365
+ context: issue.message
366
+ });
161
367
  }
368
+ const data = parsed.data;
162
369
 
163
370
  if (await this.deps.giftRepository.existsByCheckoutSessionId(data.stripeCheckoutSessionId)) {
164
371
  return false;
@@ -174,7 +381,7 @@ export class GiftService {
174
381
  buyerMemberId: member?.id ?? null,
175
382
  tierId: data.tierId,
176
383
  cadence: data.cadence,
177
- duration,
384
+ duration: data.duration,
178
385
  currency: data.currency,
179
386
  amount: data.amount,
180
387
  stripeCheckoutSessionId: data.stripeCheckoutSessionId,
@@ -198,7 +405,7 @@ export class GiftService {
198
405
  currency: data.currency,
199
406
  tierName: tier.name,
200
407
  cadence: data.cadence,
201
- duration
408
+ duration: data.duration
202
409
  });
203
410
  } catch (err) {
204
411
  logging.error('Failed to notify staff of gift purchase', err);
@@ -210,7 +417,7 @@ export class GiftService {
210
417
  token: data.token,
211
418
  tierName: tier.name,
212
419
  cadence: data.cadence,
213
- duration,
420
+ duration: data.duration,
214
421
  expiresAt: gift.expiresAt
215
422
  });
216
423
  } catch (err) {
@@ -220,11 +427,7 @@ export class GiftService {
220
427
  return true;
221
428
  }
222
429
 
223
- async getByToken(token: string): Promise<Gift | null> {
224
- return this.deps.giftRepository.getByToken(token);
225
- }
226
-
227
- async assertRedeemable(gift: Gift, memberStatus: string | null): Promise<Gift> {
430
+ private assertRedeemable(gift: Gift, memberStatus: string | null): Gift {
228
431
  const redeemableCheck = gift.checkRedeemable(memberStatus);
229
432
 
230
433
  if (!redeemableCheck.redeemable) {
@@ -267,61 +470,36 @@ export class GiftService {
267
470
  return gift;
268
471
  }
269
472
 
270
- async getRedeemable(token: string, memberStatus: string | null): Promise<Gift> {
271
- const gift = await this.deps.giftRepository.getByToken(token);
473
+ async getRedeemable(input: {token: string; memberStatus: string | null}): Promise<GiftRedemption> {
474
+ const gift = await this.deps.giftRepository.getByToken(input.token);
272
475
 
273
476
  if (!gift) {
274
477
  throw new errors.NotFoundError({message: tpl(errorMessages.giftNotFound)});
275
478
  }
276
479
 
277
- await this.assertRedeemable(gift, memberStatus);
480
+ this.assertRedeemable(gift, input.memberStatus);
278
481
 
279
- return gift;
482
+ return this.serializeRedemption(gift);
280
483
  }
281
484
 
282
- async redeem(token: string, memberId: string, options: {transacting?: {executionPromise: Promise<unknown>}; newMember?: boolean} = {}): Promise<Gift> {
283
- const run = async (transacting: unknown) => {
284
- const member = await this.deps.memberRepository.get({id: memberId}, {transacting, forUpdate: true});
285
- if (!member) {
286
- throw new errors.NotFoundError({message: `Member not found: ${memberId}`});
287
- }
288
-
289
- const gift = await this.deps.giftRepository.getByToken(token, {transacting, forUpdate: true});
290
- if (!gift) {
291
- throw new errors.NotFoundError({message: tpl(errorMessages.giftNotFound)});
292
- }
293
-
294
- if (options.newMember) {
295
- await this.assertRedeemable(gift, null);
296
- } else {
297
- await this.assertRedeemable(gift, member.get('status'));
298
- }
299
-
300
- const redeemed = gift.redeem({memberId});
301
-
302
- await this.deps.memberRepository.update({
303
- products: [{
304
- id: redeemed.tierId,
305
- expiry_at: redeemed.consumesAt
306
- }],
307
- status: 'gift'
308
- }, {id: memberId, transacting});
309
-
310
- await this.deps.giftRepository.update(redeemed, {transacting});
311
-
312
- // Gift members receive the paid welcome email, as they receive access to paid content
313
- await this.deps.memberRepository.triggerMemberSignupAutomation(
314
- memberId,
315
- member.get('email'),
316
- 'paid',
317
- {transacting}
318
- );
485
+ async redeem(input: {
486
+ token: string;
487
+ memberId: string;
488
+ transacting?: Knex.Transaction;
489
+ newMember?: boolean;
490
+ }): Promise<GiftRedemption> {
491
+ const run = async (transacting: Knex.Transaction) => {
492
+ const {redeemed, member} = await this.redeemGift(input.token, input.memberId, {
493
+ transacting,
494
+ newMember: input.newMember
495
+ });
496
+ const redemption = await this.serializeRedemption(redeemed);
319
497
 
320
- return {redeemed, member};
498
+ return {redeemed, member, redemption};
321
499
  };
322
500
 
323
- const {redeemed, member} = options.transacting
324
- ? await run(options.transacting)
501
+ const {redeemed, member, redemption} = input.transacting
502
+ ? await run(input.transacting)
325
503
  : await this.deps.giftRepository.transaction(run);
326
504
 
327
505
  const notify = async () => {
@@ -348,31 +526,72 @@ export class GiftService {
348
526
  await this.deps.giftReminderScheduler.scheduleFor(redeemed);
349
527
  };
350
528
 
351
- if (options.transacting) {
529
+ if (input.transacting) {
352
530
  // Only notify once the transaction has finished
353
- options.transacting.executionPromise.then(notify, () => {});
531
+ input.transacting.executionPromise.then(notify, () => {});
354
532
  } else {
355
533
  await notify();
356
534
  }
357
535
 
358
- return redeemed;
536
+ return redemption;
359
537
  }
360
538
 
361
- async getActiveByMember(memberId: string, options: {transacting?: unknown} = {}): Promise<Gift | null> {
539
+ private async redeemGift(token: string, memberId: string, options: {transacting: Knex.Transaction; newMember?: boolean}): Promise<{redeemed: Gift; member: MemberModel}> {
540
+ const {transacting} = options;
541
+ const member = await this.deps.memberRepository.get({id: memberId}, {transacting, forUpdate: true});
542
+ if (!member) {
543
+ throw new errors.NotFoundError({message: `Member not found: ${memberId}`});
544
+ }
545
+
546
+ const gift = await this.deps.giftRepository.getByToken(token, {transacting, forUpdate: true});
547
+ if (!gift) {
548
+ throw new errors.NotFoundError({message: tpl(errorMessages.giftNotFound)});
549
+ }
550
+
551
+ if (options.newMember) {
552
+ this.assertRedeemable(gift, null);
553
+ } else {
554
+ this.assertRedeemable(gift, member.get('status'));
555
+ }
556
+
557
+ const redeemed = gift.redeem({memberId});
558
+
559
+ await this.deps.memberRepository.update({
560
+ products: [{
561
+ id: redeemed.tierId,
562
+ expiry_at: redeemed.consumesAt
563
+ }],
564
+ status: 'gift'
565
+ }, {id: memberId, transacting});
566
+
567
+ await this.deps.giftRepository.update(redeemed, {transacting});
568
+
569
+ // Gift members receive the paid welcome email, as they receive access to paid content
570
+ await this.deps.memberRepository.triggerMemberSignupAutomation(
571
+ memberId,
572
+ member.get('email'),
573
+ 'paid',
574
+ {transacting}
575
+ );
576
+
577
+ return {redeemed, member};
578
+ }
579
+
580
+ private async getActiveByMember(memberId: string, options: {transacting?: Knex.Transaction} = {}): Promise<Gift | null> {
362
581
  if (!memberId) {
363
582
  return null;
364
583
  }
365
584
  return this.deps.giftRepository.getActiveByMember(memberId, options);
366
585
  }
367
586
 
368
- async getActiveByMembers(memberIds: string[], options: {transacting?: unknown} = {}): Promise<Map<string, Gift>> {
587
+ private async getActiveByMembers(memberIds: string[], options: {transacting?: Knex.Transaction} = {}): Promise<Map<string, Gift>> {
369
588
  if (!memberIds || memberIds.length === 0) {
370
589
  return new Map();
371
590
  }
372
591
  return this.deps.giftRepository.getActiveByMembers(memberIds, options);
373
592
  }
374
593
 
375
- getRemainingActiveDays(gift: Gift, now: Date = new Date()): number {
594
+ private getRemainingActiveDays(gift: Gift, now: Date = new Date()): number {
376
595
  if (!gift.isRedeemed() || !gift.consumesAt || gift.isConsumed()) {
377
596
  return 0;
378
597
  }
@@ -382,12 +601,89 @@ export class GiftService {
382
601
  return Math.max(0, diffDays);
383
602
  }
384
603
 
385
- async reassignRedeemer(
386
- giftId: string,
387
- memberId: string,
388
- options: {transacting?: unknown} = {}
389
- ): Promise<Gift> {
390
- const run = async (transacting: unknown): Promise<Gift> => {
604
+ async preparePaidContinuation({
605
+ memberId,
606
+ memberStatus
607
+ }: {
608
+ memberId: string;
609
+ memberStatus: string;
610
+ }): Promise<GiftContinuation> {
611
+ if (memberStatus !== 'gift') {
612
+ throw new errors.BadRequestError({
613
+ message: 'Bad Request.',
614
+ context: 'Member does not have an active gift subscription'
615
+ });
616
+ }
617
+
618
+ const gift = await this.getActiveByMember(memberId);
619
+
620
+ if (!gift) {
621
+ throw new errors.BadRequestError({
622
+ message: 'Bad Request.',
623
+ context: 'No active gift subscription found for member'
624
+ });
625
+ }
626
+
627
+ const remainingDays = this.getRemainingActiveDays(gift);
628
+
629
+ return {
630
+ tierId: gift.tierId,
631
+ cadence: gift.cadence,
632
+ trialDays: remainingDays > 0 ? Math.min(remainingDays, 730) : null
633
+ };
634
+ }
635
+
636
+ async getMemberPresentations(memberIds: string[]): Promise<Map<string, GiftMemberPresentation>> {
637
+ const gifts = await this.getActiveByMembers(memberIds);
638
+ const presentations = new Map<string, GiftMemberPresentation>();
639
+
640
+ for (const [memberId, gift] of gifts) {
641
+ presentations.set(memberId, {
642
+ cadence: gift.cadence,
643
+ currency: gift.currency,
644
+ amount: gift.amount
645
+ });
646
+ }
647
+
648
+ return presentations;
649
+ }
650
+
651
+ async getPreview(token: string): Promise<GiftPreview | null> {
652
+ const gift = await this.deps.giftRepository.getByToken(token);
653
+
654
+ if (!gift) {
655
+ return null;
656
+ }
657
+
658
+ const tier = await this.deps.tiersService.api.read(gift.tierId);
659
+
660
+ if (!tier) {
661
+ throw new errors.NotFoundError({message: `Tier not found for gift: ${gift.token}`});
662
+ }
663
+
664
+ const tierJSON = tier.toJSON();
665
+
666
+ return {
667
+ cadence: gift.cadence,
668
+ duration: gift.duration,
669
+ tier: {
670
+ id: tierJSON.id,
671
+ name: tierJSON.name
672
+ }
673
+ };
674
+ }
675
+
676
+ browsePurchaseEvents(options?: GiftEventBrowseOptions, filter?: unknown): Promise<GiftEventPage> {
677
+ return this.deps.giftRepository.browsePurchaseEvents(options, filter);
678
+ }
679
+
680
+ browseRedemptionEvents(options?: GiftEventBrowseOptions, filter?: unknown): Promise<GiftEventPage> {
681
+ return this.deps.giftRepository.browseRedemptionEvents(options, filter);
682
+ }
683
+
684
+ async reassignRedeemer(input: {giftId: string; memberId: string; transacting?: Knex.Transaction}): Promise<void> {
685
+ const {giftId, memberId} = input;
686
+ const run = async (transacting: Knex.Transaction): Promise<Gift> => {
391
687
  const gift = await this.deps.giftRepository.getById(giftId, {transacting, forUpdate: true});
392
688
 
393
689
  if (!gift) {
@@ -455,12 +751,12 @@ export class GiftService {
455
751
  return reassignedGift;
456
752
  };
457
753
 
458
- return options.transacting
459
- ? run(options.transacting)
460
- : this.deps.giftRepository.transaction(run);
754
+ await (input.transacting
755
+ ? run(input.transacting)
756
+ : this.deps.giftRepository.transaction(run));
461
757
  }
462
758
 
463
- async refund(paymentIntentId: string): Promise<boolean> {
759
+ async handlePaymentRefund({paymentIntentId}: {paymentIntentId: string}): Promise<boolean> {
464
760
  const gift = await this.deps.giftRepository.getByPaymentIntentId(paymentIntentId);
465
761
 
466
762
  if (!gift) {
@@ -491,8 +787,18 @@ export class GiftService {
491
787
  return true;
492
788
  }
493
789
 
494
- async consume(token: string, options: {transacting?: unknown} = {}): Promise<Gift | null> {
495
- const run = async (transacting: unknown) => {
790
+ async handlePaidSubscriptionActivation(memberId: string): Promise<boolean> {
791
+ const gift = await this.getActiveByMember(memberId);
792
+
793
+ if (!gift) {
794
+ return false;
795
+ }
796
+
797
+ return Boolean(await this.consume(gift.token));
798
+ }
799
+
800
+ private async consume(token: string, options: {transacting?: Knex.Transaction} = {}): Promise<Gift | null> {
801
+ const run = async (transacting: Knex.Transaction) => {
496
802
  // Fetch with a row lock to prevent race conditions under concurrency
497
803
  const gift = await this.deps.giftRepository.getByToken(token, {transacting, forUpdate: true});
498
804
 
@@ -697,4 +1003,32 @@ export class GiftService {
697
1003
 
698
1004
  return true;
699
1005
  }
1006
+
1007
+ private async serializeRedemption(gift: Gift): Promise<GiftRedemption> {
1008
+ const tier = await this.deps.tiersService.api.read(gift.tierId);
1009
+
1010
+ if (!tier) {
1011
+ throw new errors.InternalServerError({
1012
+ message: `Tier ${gift.tierId} not found for gift: ${gift.token}`
1013
+ });
1014
+ }
1015
+
1016
+ const tierJSON = tier.toJSON();
1017
+
1018
+ return {
1019
+ token: gift.token,
1020
+ cadence: gift.cadence,
1021
+ duration: gift.duration,
1022
+ currency: gift.currency,
1023
+ amount: gift.amount,
1024
+ expires_at: gift.expiresAt,
1025
+ consumes_at: gift.consumesAt,
1026
+ tier: {
1027
+ id: tierJSON.id,
1028
+ name: tierJSON.name,
1029
+ description: tierJSON.description,
1030
+ benefits: tierJSON.benefits
1031
+ }
1032
+ };
1033
+ }
700
1034
  }