ghost 6.54.1 → 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 (356) hide show
  1. package/components/tryghost-adapter-base-scheduling-0.2.0.tgz +0 -0
  2. package/components/tryghost-custom-field-types-0.0.0.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/boot.js +3 -6
  9. package/core/built/admin/assets/{Combination-CJG7Flp-.js → Combination-BCm9FL4o.js} +1 -1
  10. package/core/built/admin/assets/{PolarAngleAxis-BGjf7sBu.js → PolarAngleAxis-C4qtHy1B.js} +1 -1
  11. package/core/built/admin/assets/{RadialBarChart-zeFf9mBz.js → RadialBarChart-Bh09Stsj.js} +1 -1
  12. package/core/built/admin/assets/{account-migration-BklF2I_I.js → account-migration-D3O_yVck.js} +1 -1
  13. package/core/built/admin/assets/activitypub/{account-migration-CNovr6W-.js → account-migration-D8FbP-Z-.js} +7 -7
  14. package/core/built/admin/assets/activitypub/activitypub.js +6 -6
  15. package/core/built/admin/assets/activitypub/{avatar-B3DEDB1l.js → avatar-11EA1VSs.js} +2 -2
  16. package/core/built/admin/assets/activitypub/{avatar-flipboard-Cqrg0XMx.js → avatar-flipboard-jfbLG-xe.js} +2 -2
  17. package/core/built/admin/assets/activitypub/{bluesky-sharing-Ceslb1_M.js → bluesky-sharing-f1jTQe2P.js} +16 -16
  18. package/core/built/admin/assets/activitypub/{content-formatters-BpQARh28.js → content-formatters-nmTOPY9M.js} +3 -3
  19. package/core/built/admin/assets/activitypub/{copy-D9UWIAan.js → copy-BCwo9ep3.js} +2 -2
  20. package/core/built/admin/assets/activitypub/{dist-DHAjBcV3.js → dist-BEfX-0aW.js} +12 -12
  21. package/core/built/admin/assets/activitypub/{domain-C1qQvJ8C.js → domain-AaDlZlrA.js} +9 -9
  22. package/core/built/admin/assets/activitypub/{edit-profile-BcH-jEfM.js → edit-profile-DPi8Tl5M.js} +945 -945
  23. package/core/built/admin/assets/activitypub/{explore-AxwBh9dI.js → explore-CxTzjILQ.js} +8 -8
  24. package/core/built/admin/assets/activitypub/{feed-BaOlbh7a.js → feed-DPv5x2MX.js} +33 -33
  25. package/core/built/admin/assets/activitypub/{field-B7mYYQpN.js → field-DbgA8lat.js} +3 -3
  26. package/core/built/admin/assets/activitypub/{hash-BC9Q0N4c.js → hash-BWuZoYkA.js} +2 -2
  27. package/core/built/admin/assets/activitypub/{inbox-DQ4ZnC1m.js → inbox-35VDQ2K2.js} +2 -2
  28. package/core/built/admin/assets/activitypub/{inbox-vXez-5uJ.js → inbox-DSrF_i1O.js} +85 -85
  29. package/core/built/admin/assets/activitypub/{label-Chv5T8bw.js → label-CPjFBbo7.js} +3 -3
  30. package/core/built/admin/assets/activitypub/layout-CTgrezN7.js +1 -0
  31. package/core/built/admin/assets/activitypub/{moderation-DtSM2i1I.js → moderation-BkDC1XVZ.js} +11 -11
  32. package/core/built/admin/assets/activitypub/{note-Dckp4S-2.js → note-xEyH4xT-.js} +7 -7
  33. package/core/built/admin/assets/activitypub/{notifications-CG5T22vg.js → notifications-DClaL3ui.js} +9 -9
  34. package/core/built/admin/assets/activitypub/{preferences-D_799jdl.js → preferences-DpnOa-4Z.js} +20 -20
  35. package/core/built/admin/assets/activitypub/{profile-D2JfXBQl.js → profile-Dvi9mz-6.js} +237 -237
  36. package/core/built/admin/assets/activitypub/{reply-CpdMeAO5.js → reply-Cs9ySd6Z.js} +2 -2
  37. package/core/built/admin/assets/activitypub/{routes-CCHe2dQp.js → routes-DFBeTnfn.js} +1139 -1150
  38. package/core/built/admin/assets/activitypub/{separator-BI1FLsYM.js → separator-C5u_VjzY.js} +3 -3
  39. package/core/built/admin/assets/activitypub/{settings-kx-9Enkz.js → settings-CV2fFu6D.js} +13 -13
  40. package/core/built/admin/assets/activitypub/{site-x4_IjVJ0.js → site-_TgDd_EM.js} +81 -81
  41. package/core/built/admin/assets/activitypub/{step-1-DIDg4aHb.js → step-1-BfnNL2oF.js} +5 -5
  42. package/core/built/admin/assets/activitypub/{step-2-BfGe8BYt.js → step-2-DLndZkO-.js} +6 -6
  43. package/core/built/admin/assets/activitypub/{step-3-mE9spAqw.js → step-3-A-PZTtRE.js} +10 -10
  44. package/core/built/admin/assets/activitypub/{tabs-CyPRQQ1N.js → tabs-BUbz7tea.js} +321 -319
  45. package/core/built/admin/assets/activitypub/{topic-filter-DLHBZT93.js → topic-filter-BdX_xYYT.js} +3 -3
  46. package/core/built/admin/assets/activitypub/{use-activity-pub-queries-VzbN99u_.js → use-activity-pub-queries-EwuRemHh.js} +2 -2
  47. package/core/built/admin/assets/activitypub/{use-navigate-with-base-path-CAwXTmqH.js → use-navigate-with-base-path-cy4ACBc4.js} +92 -90
  48. package/core/built/admin/assets/activitypub/{use-reply-chain-data-BAB6fNGt.js → use-reply-chain-data-CyFcGfan.js} +5 -5
  49. package/core/built/admin/assets/activitypub/{x-C8tAnXly.js → x-7zk8mV2p.js} +2 -2
  50. package/core/built/admin/assets/audience-CkBb2LlO.js +1 -0
  51. package/core/built/admin/assets/{automated-emails-of1MvVsv.js → automated-emails-6t2bIRWf.js} +2 -2
  52. package/core/built/admin/assets/{automations-B7-y3dXt.js → automations-DhKeS_U5.js} +1 -1
  53. package/core/built/admin/assets/{automations-DRFjW5fw.js → automations-cMllU545.js} +1 -1
  54. package/core/built/admin/assets/{bluesky-sharing-BbbjWdzs.js → bluesky-sharing-DPGpyD-G.js} +1 -1
  55. package/core/built/admin/assets/{box-BWbhXh21.js → box-By_EgGtm.js} +1 -1
  56. package/core/built/admin/assets/{breadcrumb-B-M9Ip_2.js → breadcrumb-DIfcF5ow.js} +1 -1
  57. package/core/built/admin/assets/build-CsiH26uS.js +1 -0
  58. package/core/built/admin/assets/{card-DGoDJzrn.js → card-aUHX0hyR.js} +1 -1
  59. package/core/built/admin/assets/{chart-BuRT2Y6p.js → chart-BnXyfH11.js} +1 -1
  60. package/core/built/admin/assets/{chart-helpers-xiSsW8r0.js → chart-helpers-DV0FkdcD.js} +1 -1
  61. package/core/built/admin/assets/{checkbox-EhJKHFdZ.js → checkbox-BIl077a0.js} +1 -1
  62. package/core/built/admin/assets/{chunk.361.3cdb584b973959e52ccc.js → chunk.332.05991a101d67f732dec3.js} +16 -12
  63. package/core/built/admin/assets/chunk.524.87b52f4fada529df2c4c.js +25 -0
  64. package/core/built/admin/assets/chunk.582.340bcca873e806cbd4bb.js +27 -0
  65. package/core/built/admin/assets/{code-editor-view-CZtJFGhR.js → code-editor-view-C0aWNwdl.js} +1 -1
  66. package/core/built/admin/assets/comments-qwhIjH3c.js +1 -0
  67. package/core/built/admin/assets/{container-BYz2Ed39.js → container-LiZf3XdY.js} +1 -1
  68. package/core/built/admin/assets/{data-list-16rkJYYi.js → data-list-CupF7bjM.js} +1 -1
  69. package/core/built/admin/assets/{date-range-select-CTlyo8zW.js → date-range-select-dXc-GsVe.js} +1 -1
  70. package/core/built/admin/assets/{disabled-sources-indicator-qs2GfLvl.js → disabled-sources-indicator-DYAXW-6V.js} +1 -1
  71. package/core/built/admin/assets/dist-BUKmD51u.js +1 -0
  72. package/core/built/admin/assets/{dist-2Tnz2Yfq.js → dist-CNlMj7ML.js} +1 -1
  73. package/core/built/admin/assets/{dist-DGzPJ_bO.js → dist-Cb5RSHSg.js} +1 -1
  74. package/core/built/admin/assets/{dist-P_ohUlTi.js → dist-CqO2lWS0.js} +1 -1
  75. package/core/built/admin/assets/{domain-CPNXKVOV.js → domain-B5ogXUBa.js} +1 -1
  76. package/core/built/admin/assets/{dropzone-BmCXnw6J.js → dropzone-BnFG7dDP.js} +1 -1
  77. package/core/built/admin/assets/{edit-profile-SGrJve5M.js → edit-profile-B07jCZy9.js} +1 -1
  78. package/core/built/admin/assets/editor-DDYSOXGN.js +8 -0
  79. package/core/built/admin/assets/{empty-indicator-oX1u1Pkn.js → empty-indicator-WB1WgT9h.js} +1 -1
  80. package/core/built/admin/assets/{explore-CUYLv9G9.js → explore-Ca1y8HW_.js} +1 -1
  81. package/core/built/admin/assets/{feed-CPdJMttL.js → feed-x4iFMBP4.js} +1 -1
  82. package/core/built/admin/assets/field-ChCAabZg.js +1 -0
  83. package/core/built/admin/assets/filter-sources-LdtH1e-T.js +1 -0
  84. package/core/built/admin/assets/{filters-bXkkWro8.js → filters-CLehPo_D.js} +1 -1
  85. package/core/built/admin/assets/filters-CjVKRP8U.js +1 -0
  86. package/core/built/admin/assets/{ghost-05f9a7182ed56903dba05878a5b8422b.js → ghost-7f01131115186d1af84ead76e79646dd.js} +275 -255
  87. package/core/built/admin/assets/{ghost-3063eae29e6c9cc6d6e7131928a27da0.css → ghost-d38c30b866e59f2e06a357b5eb844fe5.css} +1 -1
  88. package/core/built/admin/assets/{ghost-dark-c8d0ab7f515b2010c173e23e9b32ea8a.css → ghost-dark-1bffd5099ba7a749303133efacf8480b.css} +1 -1
  89. package/core/built/admin/assets/gift-link-modal-Buqh2vXh.js +1 -0
  90. package/core/built/admin/assets/{gift-links-nBxJjZiQ.js → gift-links-CsUhefWG.js} +1 -1
  91. package/core/built/admin/assets/growth-DajOBdN0.js +1 -0
  92. package/core/built/admin/assets/growth-Dy2UOSay.js +3 -0
  93. package/core/built/admin/assets/{hooks-BApwwBE0.js → hooks-CixO01lL.js} +1 -1
  94. package/core/built/admin/assets/{inbox-Xx1CdnfO.js → inbox-BUOLDoji.js} +13 -13
  95. package/core/built/admin/assets/index-08n05-rc.js +67 -0
  96. package/core/built/admin/assets/index-DgUzgFvS.css +1 -0
  97. package/core/built/admin/assets/{inline-EMAm3okk.js → inline-Cw-gigDW.js} +1 -1
  98. package/core/built/admin/assets/input-group-uSK3XT7b.js +1 -0
  99. package/core/built/admin/assets/{input-surface-BpAjZXY5.js → input-surface-BdOe4KzO.js} +1 -1
  100. package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +9132 -9087
  101. package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +121 -121
  102. package/core/built/admin/assets/koenig-lexical/style.css +1 -1
  103. package/core/built/admin/assets/{koenig-lexical-B61IALf5.js → koenig-lexical-BFc4yFed.js} +82 -82
  104. package/core/built/admin/assets/{kpi-card-DCdlyaSS.js → kpi-card-BN-E6WYX.js} +1 -1
  105. package/core/built/admin/assets/{kpi-card-BlZaG1Lo.js → kpi-card-CCt-qeWD.js} +1 -1
  106. package/core/built/admin/assets/{kpi-helpers-DpAuS1Nb.js → kpi-helpers-DlxTbTs4.js} +1 -1
  107. package/core/built/admin/assets/{kpi-tabs-Dme44bQS.js → kpi-tabs-BlcUttpn.js} +1 -1
  108. package/core/built/admin/assets/{label-dSs5aepE.js → label-AVrxwmsz.js} +1 -1
  109. package/core/built/admin/assets/label-picker-DVnpu_IF.js +1 -0
  110. package/core/built/admin/assets/labels-DAIqKed7.js +1 -0
  111. package/core/built/admin/assets/layout-B0YLh7R9.js +1 -0
  112. package/core/built/admin/assets/link-helpers-D9LsDOkG.js +1 -0
  113. package/core/built/admin/assets/{list-page-B6ANtzkd.js → list-page-CdsnQR9N.js} +1 -1
  114. package/core/built/admin/assets/member-custom-fields-C8NpZYza.js +1 -0
  115. package/core/built/admin/assets/member-detail-CIVAdKXf.js +1 -0
  116. package/core/built/admin/assets/members-oLPiSkXD.js +10 -0
  117. package/core/built/admin/assets/modals-CB7v-k3T.js +31 -0
  118. package/core/built/admin/assets/moderation-CQ3urTFE.js +1 -0
  119. package/core/built/admin/assets/newsletter-rbeDX9Wk.js +1 -0
  120. package/core/built/admin/assets/{newsletters-D1jbtV-h.js → newsletters-BXJbJgA-.js} +1 -1
  121. package/core/built/admin/assets/newsletters-vt3Ni1Ov.js +1 -0
  122. package/core/built/admin/assets/note-Dk877BTv.js +1 -0
  123. package/core/built/admin/assets/notifications-CPlLjPxO.js +1 -0
  124. package/core/built/admin/assets/{offers-CrY6nSRR.js → offers-DEniVRD5.js} +1 -1
  125. package/core/built/admin/assets/onboarding-route-DdStOtjy.js +1 -0
  126. package/core/built/admin/assets/overview--4V-kx9c.js +1 -0
  127. package/core/built/admin/assets/overview-DSLv30oz.js +1 -0
  128. package/core/built/admin/assets/{page-header-op0eN36I.js → page-header-pp4dWmOS.js} +1 -1
  129. package/core/built/admin/assets/{pages-BbNWFcGj.js → pages-B4goRPUn.js} +1 -1
  130. package/core/built/admin/assets/{post-analytics-WwRDEnrV.js → post-analytics-BMJrI81R.js} +1 -1
  131. package/core/built/admin/assets/post-analytics-header-pU2m9ZuV.js +1 -0
  132. package/core/built/admin/assets/post-analytics-provider-Btml5vJI.js +1 -0
  133. package/core/built/admin/assets/{post-share-modal-Dx-MEjhI.js → post-share-modal-AcUZkbfL.js} +1 -1
  134. package/core/built/admin/assets/{posts-EWpZIjkr.js → posts-DhXE7b6r.js} +1 -1
  135. package/core/built/admin/assets/preferences-Ru4CRPHC.js +5 -0
  136. package/core/built/admin/assets/profile-BNb1oj7D.js +1 -0
  137. package/core/built/admin/assets/{referrers-BCfFYSHc.js → referrers-pmJT4k3-.js} +1 -1
  138. package/core/built/admin/assets/{router-provider-C1R4AZ2o.js → router-provider-BmEZf6c6.js} +1 -1
  139. package/core/built/admin/assets/select-B0HteHrp.js +1 -0
  140. package/core/built/admin/assets/settings-B1C9Q3wo.js +4 -0
  141. package/core/built/admin/assets/{settings-H6HxFgfI.js → settings-BGgk7oCI.js} +1 -1
  142. package/core/built/admin/assets/{shade-app-B3heOAfz-CGXQwWeO.js → shade-app-B3heOAfz-BhQXxNkT.js} +1 -1
  143. package/core/built/admin/assets/{share-modal-BJCadJyp.js → share-modal-BeuydaBf.js} +1 -1
  144. package/core/built/admin/assets/social-urls-DA2lXrhW.js +8 -0
  145. package/core/built/admin/assets/{sort-button-DyYlfYp7.js → sort-button-Df4t2AL8.js} +1 -1
  146. package/core/built/admin/assets/{source-icon-_ubRDbg_.js → source-icon-ajoaKb41.js} +1 -1
  147. package/core/built/admin/assets/{stack-xiN0eQ6Z.js → stack-CIfOAAVf.js} +1 -1
  148. package/core/built/admin/assets/{stats-BQZMGMvn.js → stats-C_lfxZAw.js} +1 -1
  149. package/core/built/admin/assets/{stats-view-Doei-qCX.js → stats-view-Cxot1iD8.js} +1 -1
  150. package/core/built/admin/assets/{step-1-IKiLBPoC.js → step-1-ogAA4lDr.js} +1 -1
  151. package/core/built/admin/assets/{step-2-jJ41PsDN.js → step-2-C5Xini-7.js} +1 -1
  152. package/core/built/admin/assets/{step-3-CaqFAEqf.js → step-3-CZv1FV81.js} +2 -2
  153. package/core/built/admin/assets/{switch-t-WyENU2.js → switch-CyQcwMAv.js} +1 -1
  154. package/core/built/admin/assets/{table-DxUL78jU.js → table-DFj1wiYv.js} +1 -1
  155. package/core/built/admin/assets/tabs-D4y1gqUd.js +1 -0
  156. package/core/built/admin/assets/{tags-CNujGwkT.js → tags-Dmft-Y4O.js} +1 -1
  157. package/core/built/admin/assets/{text-DwtjwGkh.js → text-DU9Hv33_.js} +1 -1
  158. package/core/built/admin/assets/textarea-d9qEkMOP.js +1 -0
  159. package/core/built/admin/assets/{tiers-BgYLWRJv.js → tiers-DYsa4DCo.js} +1 -1
  160. package/core/built/admin/assets/{toggle-group-BeEpQ69p.js → toggle-group-R2QMFvXW.js} +1 -1
  161. package/core/built/admin/assets/{topic-filter-Bb5MOD-1.js → topic-filter-DGDxDalo.js} +1 -1
  162. package/core/built/admin/assets/{use-analytics-data-Cds6XDjg.js → use-analytics-data-C76v4LIp.js} +1 -1
  163. package/core/built/admin/assets/use-filter-params-WrdhAISv.js +1 -0
  164. package/core/built/admin/assets/{use-growth-stats-D8uf98-G.js → use-growth-stats-aNXJOmQr.js} +1 -1
  165. package/core/built/admin/assets/{use-post-referrers-CRtgyuXB.js → use-post-referrers-CG0SZioj.js} +1 -1
  166. package/core/built/admin/assets/{use-reply-chain-data-DbKYX50v.js → use-reply-chain-data-RLdPKRmS.js} +1 -1
  167. package/core/built/admin/assets/{use-simple-pagination-DC01kpo0.js → use-simple-pagination-DgcF69KI.js} +1 -1
  168. package/core/built/admin/assets/{use-tinybird-query-C7bQRJWw.js → use-tinybird-query-C1R_Kbmy.js} +1 -1
  169. package/core/built/admin/assets/{use-tinybird-token-Az4tam7_.js → use-tinybird-token-C9fPjVM_.js} +1 -1
  170. package/core/built/admin/assets/{users-CcSCQFcQ.js → users-Dkv8NQZe.js} +1 -1
  171. package/core/built/admin/assets/{virtual-list-Dhuh9fGx.js → virtual-list-BmTUzeoF.js} +1 -1
  172. package/core/built/admin/assets/web-BqW0b0_C.js +1 -0
  173. package/core/built/admin/assets/web-Cy5GKMWM.js +2 -0
  174. package/core/built/admin/index.html +18 -18
  175. package/core/frontend/services/data/fetch-data.js +10 -5
  176. package/core/frontend/services/proxy.js +7 -8
  177. package/core/frontend/services/routing/api-adapter.js +115 -0
  178. package/core/frontend/services/routing/api-adapter.ts +157 -0
  179. package/core/frontend/services/routing/collection-router.js +5 -3
  180. package/core/frontend/services/routing/controllers/index.js +1 -1
  181. package/core/frontend/services/routing/controllers/rss.js +26 -31
  182. package/core/frontend/services/routing/controllers/rss.ts +66 -0
  183. package/core/frontend/services/routing/controllers/static.js +8 -7
  184. package/core/frontend/services/routing/parent-router.js +17 -4
  185. package/core/frontend/services/routing/permalink-adapter.js +26 -0
  186. package/core/frontend/services/routing/permalink-adapter.ts +23 -0
  187. package/core/frontend/services/routing/registry.js +59 -33
  188. package/core/frontend/services/routing/router-manager.js +14 -18
  189. package/core/frontend/services/routing/static-routes-router.js +11 -13
  190. package/core/frontend/services/routing/taxonomy-router.js +6 -2
  191. package/core/server/api/endpoints/automation-action-links.js +68 -0
  192. package/core/server/api/endpoints/automation-action-links.ts +46 -0
  193. package/core/server/api/endpoints/index.js +4 -0
  194. package/core/server/api/endpoints/settings.js +0 -2
  195. package/core/server/api/endpoints/utils/serializers/output/utils/url.js +11 -1
  196. package/core/server/data/migrations/versions/6.55/2026-07-27-11-25-44-add-to-hash-to-redirects.js +51 -0
  197. package/core/server/data/migrations/versions/6.55/2026-07-28-12-08-00-remove-routes-hash-from-settings.js +5 -0
  198. 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
  199. package/core/server/data/migrations/versions/6.56/2026-08-04-14-42-39-restrict-notification-add-permission-to-administrators.js +12 -0
  200. package/core/server/data/schema/commands.js +5 -0
  201. package/core/server/data/schema/default-settings/default-settings.json +0 -4
  202. package/core/server/data/schema/fixtures/fixtures.json +2 -2
  203. package/core/server/data/schema/schema.js +6 -1
  204. package/core/server/models/post.js +10 -1
  205. package/core/server/services/automations/automations-api.js +15 -0
  206. package/core/server/services/automations/automations-api.ts +19 -0
  207. package/core/server/services/automations/automations-repository.ts +20 -0
  208. package/core/server/services/automations/database-automations-repository.js +132 -30
  209. package/core/server/services/automations/database-automations-repository.ts +141 -12
  210. package/core/server/services/automations/database-date.js +10 -0
  211. package/core/server/services/automations/database-date.ts +5 -0
  212. package/core/server/services/automations/get-scheduler-idempotency-key.js +13 -0
  213. package/core/server/services/automations/get-scheduler-idempotency-key.ts +8 -0
  214. package/core/server/services/automations/poll.js +7 -2
  215. package/core/server/services/automations/poll.ts +10 -2
  216. package/core/server/services/automations/scheduler-poll-time.js +40 -0
  217. package/core/server/services/automations/scheduler-poll-time.ts +38 -0
  218. package/core/server/services/automations/service.js +19 -3
  219. package/core/server/services/automations/service.ts +20 -3
  220. package/core/server/services/comments/comments-service-emails.js +10 -7
  221. package/core/server/services/comments/comments-service.js +9 -4
  222. package/core/server/services/email-rendering/partials/email-wrapper.hbs +3 -1
  223. package/core/server/services/email-service/email-service-wrapper.js +6 -1
  224. package/core/server/services/email-service/email-templates/partials/styles.hbs +8 -0
  225. package/core/server/services/email-service/newsletter-email-event-storage.js +41 -10
  226. package/core/server/services/gift-links/CONTEXT.md +9 -0
  227. package/core/server/services/gifts/CONTEXT.md +55 -0
  228. package/core/server/services/gifts/README.md +28 -0
  229. package/core/server/services/gifts/gift-bookshelf-repository.js +65 -45
  230. package/core/server/services/gifts/gift-bookshelf-repository.ts +203 -74
  231. package/core/server/services/gifts/gift-checkout-adapter.js +59 -0
  232. package/core/server/services/gifts/gift-checkout-offer.js +74 -0
  233. package/core/server/services/gifts/gift-checkout-offer.ts +127 -0
  234. package/core/server/services/gifts/gift-codec.js +63 -0
  235. package/core/server/services/gifts/gift-codec.ts +62 -0
  236. package/core/server/services/gifts/gift-controller.js +6 -31
  237. package/core/server/services/gifts/gift-controller.ts +6 -69
  238. package/core/server/services/gifts/gift-email-service.ts +3 -2
  239. package/core/server/services/gifts/gift-schema.js +34 -0
  240. package/core/server/services/gifts/gift-schema.ts +39 -0
  241. package/core/server/services/gifts/gift-service-wrapper.js +13 -11
  242. package/core/server/services/gifts/gift-service.js +265 -52
  243. package/core/server/services/gifts/gift-service.ts +431 -97
  244. package/core/server/services/gifts/gift.ts +16 -39
  245. package/core/server/services/lib/dynamic-redirect-manager.js +12 -3
  246. package/core/server/services/link-redirection/link-redirect-repository.js +41 -4
  247. package/core/server/services/link-redirection/link-redirect.js +2 -0
  248. package/core/server/services/link-redirection/link-redirect.ts +3 -1
  249. package/core/server/services/link-redirection/link-redirects-service.js +38 -2
  250. package/core/server/services/link-tracking/click-event.js +3 -0
  251. package/core/server/services/link-tracking/index.js +38 -6
  252. package/core/server/services/link-tracking/link-click-repository.js +35 -9
  253. package/core/server/services/link-tracking/link-click-tracking-service.js +54 -4
  254. package/core/server/services/member-attribution/outbound-link-tagger.js +3 -1
  255. package/core/server/services/member-welcome-emails/member-welcome-email-renderer.js +20 -4
  256. package/core/server/services/member-welcome-emails/service.js +15 -3
  257. package/core/server/services/members/api.js +1 -2
  258. package/core/server/services/members/import-export/csv/formula.js +16 -0
  259. package/core/server/services/members/import-export/csv/formula.ts +14 -0
  260. package/core/server/services/members/import-export/csv/index.js +3 -1
  261. package/core/server/services/members/import-export/csv/index.ts +1 -0
  262. package/core/server/services/members/import-export/import/completion-email.js +22 -3
  263. package/core/server/services/members/import-export/import/completion-email.ts +29 -7
  264. package/core/server/services/members/import-export/import/importer.js +25 -4
  265. package/core/server/services/members/import-export/import/importer.ts +44 -5
  266. package/core/server/services/members/import-export/index.js +18 -1
  267. package/core/server/services/members/import-export/index.ts +32 -3
  268. package/core/server/services/members/members-api/controllers/router-controller.js +38 -36
  269. package/core/server/services/members/members-api/members-api.js +13 -7
  270. package/core/server/services/members/members-api/repositories/event-repository.js +4 -94
  271. package/core/server/services/members/members-api/services/member-bread-service.js +3 -3
  272. package/core/server/services/members/members-api/services/payments-service.js +6 -68
  273. package/core/server/services/members/service.js +8 -1
  274. package/core/server/services/members-custom-fields/schema.js +8 -1
  275. package/core/server/services/members-custom-fields/schema.ts +8 -0
  276. package/core/server/services/members-custom-fields/values-service.js +136 -8
  277. package/core/server/services/members-custom-fields/values-service.ts +152 -10
  278. package/core/server/services/mentions/service.js +5 -1
  279. package/core/server/services/oembed/oembed-service.js +44 -3
  280. package/core/server/services/posts/posts-service.js +11 -0
  281. package/core/server/services/route-settings/dynamic-routing-service.js +21 -52
  282. package/core/server/services/route-settings/index.js +1 -16
  283. package/core/server/services/route-settings/route-settings-parser.js +25 -25
  284. package/core/server/services/route-settings/route-settings-parser.ts +27 -27
  285. package/core/server/services/route-settings/validation-errors.js +68 -4
  286. package/core/server/services/route-settings/validation-errors.ts +79 -4
  287. package/core/server/services/settings/settings-service.js +0 -18
  288. package/core/server/services/stats/posts-stats-service.js +27 -23
  289. package/core/server/services/stripe/services/webhook/charge-refunded-event-service.js +1 -1
  290. package/core/server/services/stripe/services/webhook/checkout-session-event-service.js +16 -4
  291. package/core/server/services/stripe/stripe-api.js +10 -0
  292. package/core/server/services/url/url-service-facade.js +1 -1
  293. package/core/server/services/url/url-service-facade.ts +8 -1
  294. package/core/server/web/api/endpoints/admin/routes.js +1 -0
  295. package/core/server/web/gift-preview/controller.js +10 -25
  296. package/core/shared/labs.js +2 -2
  297. package/package.json +8 -7
  298. package/pnpm-lock.yaml +28 -18
  299. package/pnpm-workspace.yaml +6 -4
  300. package/components/tryghost-kg-default-nodes-2.1.3.tgz +0 -0
  301. package/core/built/admin/assets/Growth-BTP1LthE.js +0 -3
  302. package/core/built/admin/assets/Newsletters-CSEX0myc.js +0 -1
  303. package/core/built/admin/assets/Overview-BjACEsJF.js +0 -1
  304. package/core/built/admin/assets/Web-CxxxjCcv.js +0 -2
  305. package/core/built/admin/assets/activitypub/layout-CziktJXo.js +0 -1
  306. package/core/built/admin/assets/audience-X_XsZivI.js +0 -1
  307. package/core/built/admin/assets/chunk.524.8365cd9280405c0f6216.js +0 -25
  308. package/core/built/admin/assets/chunk.582.61d078937e6102769f2c.js +0 -27
  309. package/core/built/admin/assets/comments-CvyXxOyN.js +0 -1
  310. package/core/built/admin/assets/constants-c-JNF9c2.js +0 -1
  311. package/core/built/admin/assets/dist-CjY-f5Z1.js +0 -1
  312. package/core/built/admin/assets/editor-BOG2YPUm.js +0 -8
  313. package/core/built/admin/assets/field-D0XISCK2.js +0 -1
  314. package/core/built/admin/assets/filter-sources-Bieps75g.js +0 -1
  315. package/core/built/admin/assets/filters-CuB-A7ma.js +0 -1
  316. package/core/built/admin/assets/gift-link-modal-su8MsRdg.js +0 -1
  317. package/core/built/admin/assets/growth-Ba6S5FQX.js +0 -1
  318. package/core/built/admin/assets/index-D8PbzuT9.css +0 -1
  319. package/core/built/admin/assets/index-ukW_0Uie.js +0 -67
  320. package/core/built/admin/assets/input-group-CGOXcy4l.js +0 -1
  321. package/core/built/admin/assets/label-picker-Uq91atex.js +0 -1
  322. package/core/built/admin/assets/labels-Cyayfd-e.js +0 -1
  323. package/core/built/admin/assets/layout-WUcz6tu1.js +0 -1
  324. package/core/built/admin/assets/link-helpers-tOlxmuH5.js +0 -1
  325. package/core/built/admin/assets/member-custom-fields-NCzrMuba.js +0 -1
  326. package/core/built/admin/assets/member-detail-DPWDtYnN.js +0 -1
  327. package/core/built/admin/assets/members-B9rO7qr4.js +0 -10
  328. package/core/built/admin/assets/modals-CxRaan1J.js +0 -31
  329. package/core/built/admin/assets/moderation-D8zon60E.js +0 -1
  330. package/core/built/admin/assets/multi-select-combobox-1wYm3nle.js +0 -1
  331. package/core/built/admin/assets/newsletter-DbYMRxRS.js +0 -1
  332. package/core/built/admin/assets/note-PiWPnnrh.js +0 -1
  333. package/core/built/admin/assets/notifications-DEgqqB9G.js +0 -1
  334. package/core/built/admin/assets/nql-BsgFMd93.js +0 -1
  335. package/core/built/admin/assets/onboarding-route-BzF6HeVn.js +0 -1
  336. package/core/built/admin/assets/overview-DixLeIqY.js +0 -1
  337. package/core/built/admin/assets/post-analytics-header-DsMoLx0R.js +0 -1
  338. package/core/built/admin/assets/post-analytics-provider-BJCPLMIb.js +0 -1
  339. package/core/built/admin/assets/preferences-D7-j6A4C.js +0 -5
  340. package/core/built/admin/assets/profile-Dl69QPp0.js +0 -1
  341. package/core/built/admin/assets/select-C1qDZnW4.js +0 -1
  342. package/core/built/admin/assets/settings-BbmC_Iqq.js +0 -4
  343. package/core/built/admin/assets/social-urls-BPL1nADp.js +0 -8
  344. package/core/built/admin/assets/tabs-BQcJQ1ke.js +0 -1
  345. package/core/built/admin/assets/textarea-Bjccvh_E.js +0 -1
  346. package/core/built/admin/assets/use-filter-params-DjXkhO54.js +0 -1
  347. package/core/built/admin/assets/web-LtHNiNBV.js +0 -1
  348. package/core/server/services/gifts/gift-repository.js +0 -2
  349. package/core/server/services/gifts/gift-repository.ts +0 -29
  350. package/core/server/services/route-settings/activation-bridge.js +0 -164
  351. package/core/server/services/route-settings/activation-bridge.ts +0 -210
  352. package/core/server/services/route-settings/default-settings-manager.js +0 -63
  353. package/core/server/services/route-settings/route-settings.js +0 -170
  354. package/core/server/services/route-settings/settings-loader.js +0 -60
  355. package/core/server/services/route-settings/settings-path-manager.js +0 -47
  356. package/core/server/services/route-settings/validate.js +0 -440
@@ -1,4 +1,5 @@
1
1
  const moment = require('moment-timezone');
2
+ const errors = require('@tryghost/errors');
2
3
  const logging = require('@tryghost/logging');
3
4
  const config = require('../../../shared/config');
4
5
 
@@ -177,11 +178,20 @@ class NewsletterEmailEventStorage {
177
178
 
178
179
  async handleUnsubscribed(event) {
179
180
  try {
180
- // Unsubscribe member from the specific newsletter
181
- const newsletters = await this.findNewslettersToKeep(event);
182
- await this.#membersRepository.update({newsletters}, {id: event.memberId});
181
+ const result = await this.findNewslettersToKeep(event);
183
182
 
184
- // Remove member from Mailgun's suppression list
183
+ if (result.status === 'failed') {
184
+ // Leave Mailgun's suppression in place: these events are fetched
185
+ // once and never retried, so it is the only remaining protection.
186
+ return;
187
+ }
188
+
189
+ if (result.status === 'ok') {
190
+ await this.#membersRepository.update({newsletters: result.newsletters}, {id: event.memberId});
191
+ }
192
+
193
+ // Remove member from Mailgun's suppression list, only once the local
194
+ // record reflects the unsubscribe or there is no member left to protect
185
195
  await this.#emailSuppressionList.removeUnsubscribe(event.email);
186
196
  } catch (err) {
187
197
  logging.error(err);
@@ -205,22 +215,43 @@ class NewsletterEmailEventStorage {
205
215
  }
206
216
  }
207
217
 
218
+ /**
219
+ * @typedef {{status: 'ok', newsletters: {id: string}[]}
220
+ * | {status: 'no-member'}
221
+ * | {status: 'failed'}} FindNewslettersToKeepResult
222
+ */
223
+
224
+ /**
225
+ * @param {import('./events/email-unsubscribed-event')} event
226
+ * @returns {Promise<FindNewslettersToKeepResult>}
227
+ */
208
228
  async findNewslettersToKeep(event) {
209
229
  try {
210
- const member = await this.#membersRepository.get({email: event.email}, {
230
+ const member = await this.#membersRepository.get({id: event.memberId}, {
211
231
  withRelated: ['newsletters']
212
232
  });
233
+
234
+ if (!member) {
235
+ return {status: 'no-member'};
236
+ }
237
+
213
238
  const existingNewsletters = member.related('newsletters');
214
239
 
215
240
  const email = await this.#models.Email.findOne({id: event.emailId});
216
241
  const newsletterToRemove = email.get('newsletter_id');
217
242
 
218
- return existingNewsletters.models.filter(newsletter => newsletter.id !== newsletterToRemove).map((n) => {
219
- return {id: n.id};
220
- });
243
+ return {
244
+ status: 'ok',
245
+ newsletters: existingNewsletters.models.filter(newsletter => newsletter.id !== newsletterToRemove).map((n) => {
246
+ return {id: n.id};
247
+ })
248
+ };
221
249
  } catch (err) {
222
- logging.error(err);
223
- return [];
250
+ logging.error(new errors.InternalServerError({
251
+ message: `Could not resolve newsletters to keep for unsubscribe event (member ${event.memberId}, email ${event.emailId})`,
252
+ err
253
+ }));
254
+ return {status: 'failed'};
224
255
  }
225
256
  }
226
257
 
@@ -0,0 +1,9 @@
1
+ # Gift Links
2
+
3
+ Gift Links covers shareable access to individual protected posts and pages without creating a membership.
4
+
5
+ ## Language
6
+
7
+ **Gift Link**:
8
+ A revocable link that grants its holder access to one protected post or page. It does not create or redeem a gift subscription.
9
+ _Avoid_: Redemption link, gift-subscription link
@@ -0,0 +1,55 @@
1
+ # Gift Subscriptions
2
+
3
+ Gift Subscriptions covers prepaid, fixed-duration membership access intended to be shared and claimed. Its language spans gift purchase, gift redemption, gift expiration, gift consumption, and gift continuation into a paid subscription.
4
+
5
+ ## Language
6
+
7
+ ### Roles
8
+
9
+ **Buyer**:
10
+ The person who pays for a gift subscription. A buyer need not be a member and may also become the redeemer if eligible.
11
+ _Avoid_: Purchaser, recipient, redeemer
12
+
13
+ **Redeemer**:
14
+ The member who claims a gift subscription and receives its access. The redeemer is determined at redemption, not purchase.
15
+ _Avoid_: Recipient, buyer
16
+
17
+ ### Purchase and claim
18
+
19
+ **Gift subscription**:
20
+ A prepaid, non-renewing entitlement to one membership tier for a fixed period. It begins when claimed rather than when purchased.
21
+ _Avoid_: Gift membership, recurring subscription
22
+
23
+ **Gift purchase**:
24
+ A one-time purchase that creates a gift subscription for the buyer to share.
25
+ _Avoid_: Gift checkout
26
+
27
+ **Redemption link**:
28
+ A single-use, time-limited link through which a purchased gift subscription can be claimed.
29
+ _Avoid_: Gift Link
30
+
31
+ **Gift redemption**:
32
+ The act of claiming an eligible gift subscription. The member who claims it becomes the redeemer and gifted access begins at that point.
33
+ _Avoid_: Gift activation
34
+
35
+ ### Time and lifecycle
36
+
37
+ **Gift duration**:
38
+ The total length of gifted access, measured from redemption.
39
+ _Avoid_: Gift cadence, billing period
40
+
41
+ **Gift cadence**:
42
+ The monthly or yearly membership price basis used to value a gift subscription. It does not make the gift recurring.
43
+ _Avoid_: Gift duration, renewal interval
44
+
45
+ **Gift expiration**:
46
+ The end of the claim window for a purchased gift subscription that was never redeemed.
47
+ _Avoid_: Gift consumption
48
+
49
+ **Gift consumption**:
50
+ The end of a redeemed gift subscription's role in providing access, either when its duration finishes or a paid subscription takes over.
51
+ _Avoid_: Gift expiration, cancellation
52
+
53
+ **Gift continuation**:
54
+ Starting a paid subscription from gifted access while carrying the remaining gifted time forward before billing begins.
55
+ _Avoid_: Gift renewal
@@ -0,0 +1,28 @@
1
+ # Gifts
2
+
3
+ This module owns the gift-subscription journey. Its interface is
4
+ capability-oriented; callers do not receive `Gift` entities, Bookshelf models,
5
+ transactions, or Stripe objects.
6
+
7
+ ## Interface
8
+
9
+ - `startCheckout(input)` owns the `giftSubCustomization` flag, duration
10
+ catalogue, Portal plan gate, tier/cadence validation, authoritative price,
11
+ token, success parameters, metadata, customer, and one-time payment.
12
+ - `preparePaidContinuation(input)` validates an active gift and returns the
13
+ stable tier/cadence/trial decision consumed by the normal subscription
14
+ checkout path.
15
+ - `getRedeemable(input)` and `redeem(input)` return the public redemption DTO.
16
+ - `completePurchase(input)` and `handlePaymentRefund(input)` accept normalized
17
+ payment outcomes.
18
+ - `handlePaidSubscriptionActivation(memberId)` consumes gift time when a paid
19
+ subscription takes over.
20
+ - `getMemberPresentations(memberIds)`, `getPreview(token)`,
21
+ `browsePurchaseEvents(...)`, and `browseRedemptionEvents(...)` expose
22
+ stable read models.
23
+ - `processReminders()`, `processConsumed()`, and `processExpired()` own due
24
+ lifecycle work; scheduler and HTTP triggers remain adapters.
25
+ - `reassignRedeemer(...)` is the import capability.
26
+
27
+ The `Gift`, repository, Bookshelf query, Stripe checkout, email, scheduling,
28
+ and notification collaborators are internal adapters.
@@ -5,7 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.GiftBookshelfRepository = void 0;
7
7
  const errors_1 = __importDefault(require("@tryghost/errors"));
8
- const gift_1 = require("./gift");
8
+ const mongo_utils_1 = require("@tryghost/mongo-utils");
9
+ const gift_codec_1 = require("./gift-codec");
9
10
  class GiftBookshelfRepository {
10
11
  model;
11
12
  constructor({ GiftModel }) {
@@ -60,6 +61,26 @@ class GiftBookshelfRepository {
60
61
  }
61
62
  return map;
62
63
  }
64
+ browsePurchaseEvents(options = {}, filter) {
65
+ return this.browseEvents({
66
+ options,
67
+ filter,
68
+ type: 'gift_purchase_event',
69
+ relation: 'buyer',
70
+ memberIdColumn: 'buyer_member_id',
71
+ dateColumn: 'purchased_at'
72
+ });
73
+ }
74
+ browseRedemptionEvents(options = {}, filter) {
75
+ return this.browseEvents({
76
+ options,
77
+ filter,
78
+ type: 'gift_redemption_event',
79
+ relation: 'redeemer',
80
+ memberIdColumn: 'redeemer_member_id',
81
+ dateColumn: 'redeemed_at'
82
+ });
83
+ }
63
84
  async findPendingConsumption() {
64
85
  const now = new Date();
65
86
  const collection = await this.model.findAll({
@@ -111,53 +132,52 @@ class GiftBookshelfRepository {
111
132
  return await this.model.transaction(callback);
112
133
  }
113
134
  toRow(gift) {
114
- return {
115
- token: gift.token,
116
- buyer_email: gift.buyerEmail,
117
- buyer_member_id: gift.buyerMemberId,
118
- redeemer_member_id: gift.redeemerMemberId,
119
- tier_id: gift.tierId,
120
- cadence: gift.cadence,
121
- duration: gift.duration,
122
- currency: gift.currency,
123
- amount: gift.amount,
124
- stripe_checkout_session_id: gift.stripeCheckoutSessionId,
125
- stripe_payment_intent_id: gift.stripePaymentIntentId,
126
- consumes_at: gift.consumesAt,
127
- expires_at: gift.expiresAt,
128
- status: gift.status,
129
- purchased_at: gift.purchasedAt,
130
- redeemed_at: gift.redeemedAt,
131
- consumed_at: gift.consumedAt,
132
- expired_at: gift.expiredAt,
133
- refunded_at: gift.refundedAt,
134
- consumes_soon_reminder_sent_at: gift.consumesSoonReminderSentAt
135
- };
135
+ return (0, gift_codec_1.encodeGift)(gift);
136
136
  }
137
137
  toGift(model) {
138
- const json = model.toJSON();
139
- return new gift_1.Gift({
140
- token: json.token,
141
- buyerEmail: json.buyer_email,
142
- buyerMemberId: json.buyer_member_id,
143
- redeemerMemberId: json.redeemer_member_id,
144
- tierId: json.tier_id,
145
- cadence: json.cadence,
146
- duration: json.duration,
147
- currency: json.currency,
148
- amount: json.amount,
149
- stripeCheckoutSessionId: json.stripe_checkout_session_id,
150
- stripePaymentIntentId: json.stripe_payment_intent_id,
151
- consumesAt: json.consumes_at,
152
- expiresAt: json.expires_at,
153
- status: json.status,
154
- purchasedAt: json.purchased_at,
155
- redeemedAt: json.redeemed_at,
156
- consumedAt: json.consumed_at,
157
- expiredAt: json.expired_at,
158
- refundedAt: json.refunded_at,
159
- consumesSoonReminderSentAt: json.consumes_soon_reminder_sent_at ?? null
138
+ return (0, gift_codec_1.decodeGiftRow)(model.toJSON());
139
+ }
140
+ async browseEvents({ options, filter, type, relation, memberIdColumn, dateColumn }) {
141
+ const replaceCustomFilter = (existingFilter) => (0, mongo_utils_1.replaceFilters)(existingFilter, {
142
+ custom: filter
160
143
  });
144
+ const queryOptions = {
145
+ ...options,
146
+ withRelated: [relation, 'tier'],
147
+ filter: `${memberIdColumn}:-null+custom:true`,
148
+ useBasicCount: true,
149
+ mongoTransformer: (0, mongo_utils_1.chainTransformers)(replaceCustomFilter, ...(0, mongo_utils_1.mapKeys)({
150
+ 'data.created_at': dateColumn,
151
+ 'data.member_id': memberIdColumn
152
+ }))
153
+ };
154
+ if (typeof queryOptions.order === 'string') {
155
+ queryOptions.order = queryOptions.order.replace(/created_at/g, dateColumn);
156
+ }
157
+ if (!this.model.findPage) {
158
+ throw new errors_1.default.InternalServerError({ message: 'Gift model does not support paginated event queries.' });
159
+ }
160
+ const { data: models, meta } = await this.model.findPage(queryOptions);
161
+ return {
162
+ data: models.map((model) => {
163
+ const json = model.toJSON(queryOptions);
164
+ return {
165
+ type,
166
+ data: {
167
+ id: json.id,
168
+ member: json[relation] || null,
169
+ member_id: json[memberIdColumn],
170
+ tier_name: json.tier?.name,
171
+ cadence: json.cadence,
172
+ duration: json.duration,
173
+ amount: json.amount,
174
+ currency: json.currency,
175
+ created_at: json[dateColumn]
176
+ }
177
+ };
178
+ }),
179
+ meta
180
+ };
161
181
  }
162
182
  }
163
183
  exports.GiftBookshelfRepository = GiftBookshelfRepository;
@@ -1,10 +1,100 @@
1
1
  import errors from '@tryghost/errors';
2
- import {Gift, type GiftCadence, type GiftStatus} from './gift';
3
- import type {FindPendingReminderOptions, GiftRepository, RepositoryTransactionOptions} from './gift-repository';
2
+ import {chainTransformers, mapKeys, replaceFilters} from '@tryghost/mongo-utils';
3
+ import type {Knex} from 'knex';
4
+ import {Gift} from './gift';
5
+ import {decodeGiftRow, encodeGift} from './gift-codec';
6
+ import type {GiftCadence, GiftRow} from './gift-schema';
7
+
8
+ type ParsedNqlFilter = unknown;
9
+
10
+ export interface GiftEventBrowseOptions {
11
+ filter?: string;
12
+ limit?: number | 'all';
13
+ order?: string;
14
+ page?: number;
15
+ }
16
+
17
+ export interface Pagination {
18
+ page: number;
19
+ pages: number;
20
+ limit: number | 'all';
21
+ total: number;
22
+ prev: number | null;
23
+ next: number | null;
24
+ }
25
+
26
+ export interface GiftEventData {
27
+ id: string;
28
+ member: Record<string, unknown> | null;
29
+ member_id: string | null;
30
+ tier_name?: string;
31
+ cadence: GiftCadence;
32
+ duration: number;
33
+ amount: number;
34
+ currency: string;
35
+ created_at: Date | string | null;
36
+ }
37
+
38
+ export interface GiftEventPage {
39
+ data: Array<{
40
+ type: 'gift_purchase_event' | 'gift_redemption_event';
41
+ data: GiftEventData;
42
+ }>;
43
+ meta: {
44
+ pagination?: Pagination;
45
+ };
46
+ }
47
+
48
+ export interface RepositoryTransactionOptions {
49
+ transacting?: Knex.Transaction;
50
+ forUpdate?: boolean;
51
+ }
52
+
53
+ export interface FindPendingReminderOptions {
54
+ now: Date;
55
+ reminderLeadMs: number;
56
+ reminderFloorMs: number;
57
+ transacting?: Knex.Transaction;
58
+ }
59
+
60
+ export interface GiftRepository {
61
+ existsByCheckoutSessionId(checkoutSessionId: string): Promise<boolean>;
62
+ getById(id: string, options?: RepositoryTransactionOptions): Promise<Gift | null>;
63
+ getByToken(token: string, options?: RepositoryTransactionOptions): Promise<Gift | null>;
64
+ getByPaymentIntentId(paymentIntentId: string): Promise<Gift | null>;
65
+ findPendingConsumption(): Promise<Gift[]>;
66
+ findPendingExpiration(): Promise<Gift[]>;
67
+ findPendingReminder(options: FindPendingReminderOptions): Promise<Gift[]>;
68
+ findUnsentReminders(): Promise<Gift[]>;
69
+ getActiveByMember(memberId: string, options?: RepositoryTransactionOptions): Promise<Gift | null>;
70
+ getActiveByMembers(memberIds: string[], options?: RepositoryTransactionOptions): Promise<Map<string, Gift>>;
71
+ browsePurchaseEvents(options?: GiftEventBrowseOptions, filter?: ParsedNqlFilter): Promise<GiftEventPage>;
72
+ browseRedemptionEvents(options?: GiftEventBrowseOptions, filter?: ParsedNqlFilter): Promise<GiftEventPage>;
73
+ create(gift: Gift, options?: RepositoryTransactionOptions): Promise<void>;
74
+ update(gift: Gift, options?: RepositoryTransactionOptions): Promise<void>;
75
+ transaction<T>(callback: (transacting: Knex.Transaction) => Promise<T>): Promise<T>;
76
+ }
77
+
78
+ type BookshelfSaveOptions = RepositoryTransactionOptions & {
79
+ autoRefresh?: boolean;
80
+ method?: 'update';
81
+ patch?: boolean;
82
+ };
83
+
84
+ type GiftEventQueryOptions = GiftEventBrowseOptions & {
85
+ withRelated: Array<'buyer' | 'redeemer' | 'tier'>;
86
+ filter: string;
87
+ useBasicCount: boolean;
88
+ mongoTransformer: (filter: ParsedNqlFilter) => ParsedNqlFilter;
89
+ };
90
+
91
+ type BookshelfFindOptions = RepositoryTransactionOptions & {
92
+ filter?: string;
93
+ require?: boolean;
94
+ };
4
95
 
5
96
  type BookshelfDocument<T> = {
6
- save(data: Partial<T>, options?: unknown): Promise<unknown>;
7
- set(data: Partial<T>): void;
97
+ save(data: Partial<T>, options?: BookshelfSaveOptions): Promise<BookshelfDocument<T>>;
8
98
  toJSON(): T;
9
99
  };
10
100
 
@@ -12,35 +102,31 @@ type BookshelfCollection<T> = {
12
102
  models: BookshelfDocument<T>[];
13
103
  };
14
104
 
15
- type BookshelfModel<T> = {
16
- add(data: Partial<T>, unfilteredOptions?: unknown): Promise<T>;
17
- transaction<R>(callback: (transacting: unknown) => Promise<R>): Promise<R>;
18
- findOne(data: Record<string, unknown>, unfilteredOptions?: unknown): Promise<BookshelfDocument<T> | null>;
19
- findAll(unfilteredOptions?: unknown): Promise<BookshelfCollection<T>>;
20
- };
21
-
22
- type GiftRow = {
105
+ type GiftEventBookshelfRow = {
23
106
  id: string;
24
- token: string;
25
- buyer_email: string;
26
107
  buyer_member_id: string | null;
27
108
  redeemer_member_id: string | null;
28
- tier_id: string;
109
+ buyer?: Record<string, unknown> | null;
110
+ redeemer?: Record<string, unknown> | null;
111
+ tier?: {name?: string} | null;
29
112
  cadence: GiftCadence;
30
113
  duration: number;
31
- currency: string;
32
114
  amount: number;
33
- stripe_checkout_session_id: string;
34
- stripe_payment_intent_id: string;
35
- consumes_at: Date | null;
36
- expires_at: Date;
37
- status: GiftStatus;
38
- purchased_at: Date;
39
- redeemed_at: Date | null;
40
- consumed_at: Date | null;
41
- expired_at: Date | null;
42
- refunded_at: Date | null;
43
- consumes_soon_reminder_sent_at: Date | null;
115
+ currency: string;
116
+ purchased_at: Date | string;
117
+ redeemed_at: Date | string | null;
118
+ };
119
+
120
+ type GiftEventBookshelfDocument = {
121
+ toJSON(options?: GiftEventQueryOptions): GiftEventBookshelfRow;
122
+ };
123
+
124
+ type BookshelfModel<T> = {
125
+ add(data: Partial<T>, options?: RepositoryTransactionOptions): Promise<BookshelfDocument<T>>;
126
+ transaction<R>(callback: (transacting: Knex.Transaction) => Promise<R>): Promise<R>;
127
+ findOne(data: Partial<T> & {id?: string}, options?: BookshelfFindOptions): Promise<BookshelfDocument<T> | null>;
128
+ findAll(options?: BookshelfFindOptions): Promise<BookshelfCollection<T>>;
129
+ findPage(options: GiftEventQueryOptions): Promise<{data: GiftEventBookshelfDocument[]; meta: GiftEventPage['meta']}>;
44
130
  };
45
131
 
46
132
  type GiftBookshelfModel = BookshelfModel<GiftRow>;
@@ -116,6 +202,28 @@ export class GiftBookshelfRepository implements GiftRepository {
116
202
  return map;
117
203
  }
118
204
 
205
+ browsePurchaseEvents(options: GiftEventBrowseOptions = {}, filter?: ParsedNqlFilter): Promise<GiftEventPage> {
206
+ return this.browseEvents({
207
+ options,
208
+ filter,
209
+ type: 'gift_purchase_event',
210
+ relation: 'buyer',
211
+ memberIdColumn: 'buyer_member_id',
212
+ dateColumn: 'purchased_at'
213
+ });
214
+ }
215
+
216
+ browseRedemptionEvents(options: GiftEventBrowseOptions = {}, filter?: ParsedNqlFilter): Promise<GiftEventPage> {
217
+ return this.browseEvents({
218
+ options,
219
+ filter,
220
+ type: 'gift_redemption_event',
221
+ relation: 'redeemer',
222
+ memberIdColumn: 'redeemer_member_id',
223
+ dateColumn: 'redeemed_at'
224
+ });
225
+ }
226
+
119
227
  async findPendingConsumption(): Promise<Gift[]> {
120
228
  const now = new Date();
121
229
 
@@ -179,59 +287,80 @@ export class GiftBookshelfRepository implements GiftRepository {
179
287
  });
180
288
  }
181
289
 
182
- async transaction<T>(callback: (transacting: unknown) => Promise<T>): Promise<T> {
290
+ async transaction<T>(callback: (transacting: Knex.Transaction) => Promise<T>): Promise<T> {
183
291
  return await this.model.transaction(callback);
184
292
  }
185
293
 
186
- private toRow(gift: Gift): Omit<GiftRow, 'id'> {
187
- return {
188
- token: gift.token,
189
- buyer_email: gift.buyerEmail,
190
- buyer_member_id: gift.buyerMemberId,
191
- redeemer_member_id: gift.redeemerMemberId,
192
- tier_id: gift.tierId,
193
- cadence: gift.cadence,
194
- duration: gift.duration,
195
- currency: gift.currency,
196
- amount: gift.amount,
197
- stripe_checkout_session_id: gift.stripeCheckoutSessionId,
198
- stripe_payment_intent_id: gift.stripePaymentIntentId,
199
- consumes_at: gift.consumesAt,
200
- expires_at: gift.expiresAt,
201
- status: gift.status,
202
- purchased_at: gift.purchasedAt,
203
- redeemed_at: gift.redeemedAt,
204
- consumed_at: gift.consumedAt,
205
- expired_at: gift.expiredAt,
206
- refunded_at: gift.refundedAt,
207
- consumes_soon_reminder_sent_at: gift.consumesSoonReminderSentAt
208
- };
294
+ private toRow(gift: Gift): GiftRow {
295
+ return encodeGift(gift);
209
296
  }
210
297
 
211
298
  private toGift(model: BookshelfDocument<GiftRow>): Gift {
212
- const json = model.toJSON();
213
-
214
- return new Gift({
215
- token: json.token,
216
- buyerEmail: json.buyer_email,
217
- buyerMemberId: json.buyer_member_id,
218
- redeemerMemberId: json.redeemer_member_id,
219
- tierId: json.tier_id,
220
- cadence: json.cadence,
221
- duration: json.duration,
222
- currency: json.currency,
223
- amount: json.amount,
224
- stripeCheckoutSessionId: json.stripe_checkout_session_id,
225
- stripePaymentIntentId: json.stripe_payment_intent_id,
226
- consumesAt: json.consumes_at,
227
- expiresAt: json.expires_at,
228
- status: json.status,
229
- purchasedAt: json.purchased_at,
230
- redeemedAt: json.redeemed_at,
231
- consumedAt: json.consumed_at,
232
- expiredAt: json.expired_at,
233
- refundedAt: json.refunded_at,
234
- consumesSoonReminderSentAt: json.consumes_soon_reminder_sent_at ?? null
299
+ return decodeGiftRow(model.toJSON());
300
+ }
301
+
302
+ private async browseEvents({
303
+ options,
304
+ filter,
305
+ type,
306
+ relation,
307
+ memberIdColumn,
308
+ dateColumn
309
+ }: {
310
+ options: GiftEventBrowseOptions;
311
+ filter?: ParsedNqlFilter;
312
+ type: 'gift_purchase_event' | 'gift_redemption_event';
313
+ relation: 'buyer' | 'redeemer';
314
+ memberIdColumn: 'buyer_member_id' | 'redeemer_member_id';
315
+ dateColumn: 'purchased_at' | 'redeemed_at';
316
+ }): Promise<GiftEventPage> {
317
+ const replaceCustomFilter = (existingFilter: ParsedNqlFilter): ParsedNqlFilter => replaceFilters(existingFilter, {
318
+ custom: filter
235
319
  });
320
+ const queryOptions: GiftEventQueryOptions = {
321
+ ...options,
322
+ withRelated: [relation, 'tier'],
323
+ filter: `${memberIdColumn}:-null+custom:true`,
324
+ useBasicCount: true,
325
+ mongoTransformer: chainTransformers(
326
+ replaceCustomFilter,
327
+ ...mapKeys({
328
+ 'data.created_at': dateColumn,
329
+ 'data.member_id': memberIdColumn
330
+ })
331
+ )
332
+ };
333
+
334
+ if (typeof queryOptions.order === 'string') {
335
+ queryOptions.order = queryOptions.order.replace(/created_at/g, dateColumn);
336
+ }
337
+
338
+ if (!this.model.findPage) {
339
+ throw new errors.InternalServerError({message: 'Gift model does not support paginated event queries.'});
340
+ }
341
+
342
+ const {data: models, meta} = await this.model.findPage(queryOptions);
343
+
344
+ return {
345
+ data: models.map((model) => {
346
+ const json = model.toJSON(queryOptions);
347
+
348
+ return {
349
+ type,
350
+ data: {
351
+ id: json.id,
352
+ member: json[relation] || null,
353
+ member_id: json[memberIdColumn],
354
+ tier_name: json.tier?.name,
355
+ cadence: json.cadence,
356
+ duration: json.duration,
357
+ amount: json.amount,
358
+ currency: json.currency,
359
+ created_at: json[dateColumn]
360
+ }
361
+ };
362
+ }),
363
+ meta
364
+ };
236
365
  }
237
366
  }
@@ -0,0 +1,59 @@
1
+ const logging = require('@tryghost/logging');
2
+
3
+ /**
4
+ * Stripe and Bookshelf adapter used only by the gifts module.
5
+ *
6
+ * The module interface deals in stable member/customer IDs. Stripe customer
7
+ * objects and Bookshelf models stay on this side of the seam.
8
+ */
9
+ module.exports = class GiftCheckoutAdapter {
10
+ constructor({StripeCustomerModel, getStripeApi}) {
11
+ this.StripeCustomerModel = StripeCustomerModel;
12
+ this.getStripeApi = getStripeApi;
13
+ }
14
+
15
+ async getCustomerId({memberId, email, name}) {
16
+ if (!memberId) {
17
+ return null;
18
+ }
19
+
20
+ const rows = await this.StripeCustomerModel.where({
21
+ member_id: memberId
22
+ }).query().select('customer_id');
23
+
24
+ for (const row of rows) {
25
+ try {
26
+ const customer = await this.getStripeApi().getCustomer(row.customer_id);
27
+ if (!customer.deleted) {
28
+ return customer.id;
29
+ }
30
+ } catch (err) {
31
+ logging.warn(err);
32
+ }
33
+ }
34
+
35
+ const customer = await this.getStripeApi().createCustomer({
36
+ email,
37
+ name
38
+ });
39
+
40
+ await this.StripeCustomerModel.add({
41
+ member_id: memberId,
42
+ customer_id: customer.id,
43
+ email: customer.email,
44
+ name: customer.name
45
+ });
46
+
47
+ return customer.id;
48
+ }
49
+
50
+ async createSession(data) {
51
+ const {customerId, ...sessionData} = data;
52
+ const session = await this.getStripeApi().createGiftCheckoutSession({
53
+ ...sessionData,
54
+ customer: customerId ? {id: customerId} : null
55
+ });
56
+
57
+ return session.url;
58
+ }
59
+ };