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
@@ -14,7 +14,6 @@ class PaymentsService {
14
14
  * @param {import('../../../offers/application/offers-api')} deps.offersAPI
15
15
  * @param {import('../../../stripe/stripe-api')} deps.stripeAPIService
16
16
  * @param {{get(key: string): any}} deps.settingsCache
17
- * @param {{service: import('../../../gifts/gift-service').GiftService}} deps.giftService
18
17
  */
19
18
  constructor(deps) {
20
19
  /** @private */
@@ -31,9 +30,6 @@ class PaymentsService {
31
30
  this.stripeAPIService = deps.stripeAPIService;
32
31
  /** @private */
33
32
  this.settingsCache = deps.settingsCache;
34
- /** @private */
35
- this.giftService = deps.giftService;
36
-
37
33
  DomainEvents.subscribe(OfferCreatedEvent, async (event) => {
38
34
  await this.getCouponForOffer(event.data.offer.id);
39
35
  });
@@ -65,7 +61,7 @@ class PaymentsService {
65
61
  * @param {Tier.Cadence} params.cadence
66
62
  * @param {Offer} [params.offer]
67
63
  * @param {Member} [params.member]
68
- * @param {import('../../../gifts/gift').Gift} [params.gift]
64
+ * @param {number|null} [params.giftTrialDays]
69
65
  * @param {Object.<string, any>} [params.metadata]
70
66
  * @param {string} params.successUrl
71
67
  * @param {string} params.cancelUrl
@@ -73,7 +69,7 @@ class PaymentsService {
73
69
  *
74
70
  * @returns {Promise<URL>}
75
71
  */
76
- async getPaymentLink({tier, cadence, offer, member, gift, metadata, successUrl, cancelUrl, email}) {
72
+ async getPaymentLink({tier, cadence, offer, member, giftTrialDays, metadata, successUrl, cancelUrl, email}) {
77
73
  let coupon = null;
78
74
  let trialDays = null;
79
75
  if (offer) {
@@ -94,12 +90,10 @@ class PaymentsService {
94
90
  }
95
91
  }
96
92
 
97
- // If continuing from a gift subscription, add gift remaining days as trial
98
- if (trialDays === null && gift) {
99
- const remainingDays = this.giftService.service.getRemainingActiveDays(gift);
100
- if (remainingDays > 0) {
101
- trialDays = Math.min(remainingDays, 730); // Stripe max trial period
102
- }
93
+ // Gift subscriptions owns the continuation calculation. This adapter only
94
+ // translates the stable decision into the recurring Stripe checkout.
95
+ if (trialDays === null && giftTrialDays !== undefined) {
96
+ trialDays = giftTrialDays;
103
97
  }
104
98
 
105
99
  let customer = null;
@@ -163,62 +157,6 @@ class PaymentsService {
163
157
  return session.url;
164
158
  }
165
159
 
166
- /**
167
- * @param {object} params
168
- * @param {import('../../../tiers/tier')} params.tier
169
- * @param {'month'|'year'} params.cadence
170
- * @param {number} params.duration
171
- * @param {string} params.successUrl
172
- * @param {string} params.cancelUrl
173
- * @param {object} params.metadata
174
- * @param {object} [params.member]
175
- * @param {boolean} params.isAuthenticated
176
- * @param {string} [params.email]
177
- *
178
- * @returns {Promise<string>}
179
- */
180
- async getGiftPaymentLink({tier, cadence, duration, metadata, successUrl, cancelUrl, member, isAuthenticated, email}) {
181
- let customer = null;
182
- if (member && isAuthenticated) {
183
- customer = await this.getCustomerForMember(member);
184
- }
185
-
186
- const amount = tier.getPrice(cadence);
187
- const currency = tier.currency.toLowerCase();
188
-
189
- const token = this.giftService.service.generateToken();
190
-
191
- const successUrlObj = new URL(successUrl);
192
- successUrlObj.searchParams.set('stripe', 'gift-purchase-success');
193
- successUrlObj.searchParams.set('gift_token', token);
194
- successUrlObj.searchParams.set('gift_tier', tier.id.toHexString());
195
- successUrlObj.searchParams.set('gift_cadence', cadence);
196
-
197
- const data = {
198
- amount,
199
- currency,
200
- tierName: tier.name,
201
- cadence,
202
- duration,
203
- metadata: {
204
- ...metadata,
205
- ghost_gift: 'true',
206
- gift_token: token,
207
- tier_id: tier.id.toHexString(),
208
- cadence,
209
- duration: String(duration)
210
- },
211
- successUrl: successUrlObj.toString(),
212
- cancelUrl,
213
- customer,
214
- customerEmail: !customer && email ? email : null
215
- };
216
-
217
- const session = await this.stripeAPIService.createGiftCheckoutSession(data);
218
-
219
- return session.url;
220
- }
221
-
222
160
  async getCustomerForMember(member) {
223
161
  const rows = await this.StripeCustomerModel.where({
224
162
  member_id: member.id
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DbCustomFieldValueWithField = exports.DbCustomFieldValue = exports.DbCustomField = exports.FieldStatusSchema = exports.FIELD_STATUS = void 0;
3
+ exports.DbCustomFieldValueWithField = exports.StoredCompositeValue = exports.DbCustomFieldValue = exports.DbCustomField = exports.FieldStatusSchema = exports.FIELD_STATUS = void 0;
4
4
  const zod_1 = require("zod");
5
5
  const custom_field_types_1 = require("@tryghost/custom-field-types");
6
6
  const db_date_1 = require("../../lib/db-date");
@@ -37,6 +37,13 @@ exports.DbCustomFieldValue = zod_1.z.object({
37
37
  created_at: db_date_1.DbDate,
38
38
  updated_at: db_date_1.DbDate.nullable()
39
39
  });
40
+ // A composite value once decoded: sub-field keys to values, and no narrower. Which
41
+ // sub-fields a composite has is its field type's business, not the row's.
42
+ //
43
+ // It doubles as the guard on a stored blob. A JSON column can just as easily hold an
44
+ // array, a null or a bare number, and a record rejects all three — so parsing against
45
+ // this is what narrows the type, with no assertion needed.
46
+ exports.StoredCompositeValue = zod_1.z.record(zod_1.z.string(), zod_1.z.unknown());
40
47
  // The value join a read needs: the field's identity and type travel with the
41
48
  // stored columns, so a row can be decoded without a second lookup. `type` is
42
49
  // parsed as the field-type enum, which narrows it with no cast.
@@ -44,6 +44,14 @@ export const DbCustomFieldValue = z.object({
44
44
 
45
45
  type CustomFieldValueRow = z.infer<typeof DbCustomFieldValue>;
46
46
 
47
+ // A composite value once decoded: sub-field keys to values, and no narrower. Which
48
+ // sub-fields a composite has is its field type's business, not the row's.
49
+ //
50
+ // It doubles as the guard on a stored blob. A JSON column can just as easily hold an
51
+ // array, a null or a bare number, and a record rejects all three — so parsing against
52
+ // this is what narrows the type, with no assertion needed.
53
+ export const StoredCompositeValue = z.record(z.string(), z.unknown());
54
+
47
55
  // The value join a read needs: the field's identity and type travel with the
48
56
  // stored columns, so a row can be decoded without a second lookup. `type` is
49
57
  // parsed as the field-type enum, which narrows it with no cast.
@@ -21,6 +21,21 @@ const MAX_KEY_LENGTH = 191;
21
21
  // validated by its own field type's schema, which isn't known until the key is
22
22
  // resolved to a definition.
23
23
  const ValuesInput = zod_1.z.record(zod_1.z.string().max(MAX_KEY_LENGTH), zod_1.z.unknown());
24
+ // JSON.parse as a total function. Anything unreadable — unparseable text, or the null
25
+ // a nullable column can always return — reads as `undefined`, which no value schema
26
+ // accepts. That way a corrupt blob is turned away by the same check that turns away a
27
+ // well-formed one of the wrong shape, instead of needing a branch of its own.
28
+ function decodeJson(raw) {
29
+ if (raw === null) {
30
+ return undefined;
31
+ }
32
+ try {
33
+ return JSON.parse(raw);
34
+ }
35
+ catch {
36
+ return undefined;
37
+ }
38
+ }
24
39
  /**
25
40
  * The value half of member custom fields: reading and writing what a member holds
26
41
  * for each defined field. It belongs to the member aggregate — a value is data
@@ -188,28 +203,129 @@ class CustomFieldValuesService {
188
203
  }
189
204
  return writes;
190
205
  }
206
+ /**
207
+ * @private
208
+ * The composite values this member already holds for the fields in a plan,
209
+ * keyed by field id. Composites only: a scalar has nothing to merge into.
210
+ *
211
+ * Read through the caller's executor, never the base connection. On a
212
+ * single-connection pool a read off `this.knex` inside an open transaction
213
+ * waits for the connection that transaction is holding, and hangs.
214
+ *
215
+ * A blob that won't decode is left out, so the write replaces it rather than
216
+ * merging into something unreadable.
217
+ */
218
+ async storedComposites(trx, memberId, writes) {
219
+ const composites = new Map();
220
+ // Merging is only meaningful for a value with parts, which is a fact about the
221
+ // type's shape rather than the column it lands in. Asking the storage type
222
+ // instead would sweep in the first json-backed type that isn't a record — a
223
+ // multi-select held as a list, say — and log a decode failure for every row of
224
+ // an import, over a value that reads back perfectly well.
225
+ const fieldIds = writes
226
+ .filter(({ field, value }) => value !== undefined && (0, custom_field_types_1.subFieldsOf)(field.type) !== null)
227
+ .map(({ field }) => field.id);
228
+ if (fieldIds.length === 0) {
229
+ return composites;
230
+ }
231
+ const rows = await trx(VALUES_TABLE)
232
+ .where('member_id', memberId)
233
+ .whereIn('custom_field_id', fieldIds)
234
+ .whereNotNull('value_json')
235
+ .select('custom_field_id', 'value_json')
236
+ // Merging reads a value and writes it back, so the row has to stay still in
237
+ // between: without the lock a write committing after this read is overwritten
238
+ // by the sub-fields it just replaced. knex omits this on SQLite, which needs
239
+ // no lock — its pool is a single connection, so writes never overlap.
240
+ .forUpdate();
241
+ for (const row of rows) {
242
+ // The blob is the edge here: the columns come from a query written just
243
+ // above, but whatever an earlier write left in value_json is unknown until
244
+ // it is read back. Parsing it is what narrows the type, and it rejects the
245
+ // array, null or bare number the column could equally be holding.
246
+ const stored = schema_1.StoredCompositeValue.safeParse(decodeJson(row.value_json));
247
+ if (stored.success) {
248
+ composites.set(row.custom_field_id, stored.data);
249
+ continue;
250
+ }
251
+ logging_1.default.warn(`Replacing an unreadable custom field value rather than merging into it (member '${memberId}', field '${row.custom_field_id}').`);
252
+ }
253
+ return composites;
254
+ }
255
+ /**
256
+ * @private
257
+ * The stored sub-fields with the incoming ones written over them, re-checked
258
+ * against the field type before it is handed back.
259
+ *
260
+ * The re-check is what makes this safe to write without a second thought. Both
261
+ * halves were valid when they were written, but "valid" is a moving target: the
262
+ * stored half may predate a change to the type, so it can carry a sub-field the
263
+ * type has since dropped or a bound it has since tightened. Parsing the result
264
+ * strips what no longer belongs instead of faithfully persisting it on every
265
+ * import from here on.
266
+ *
267
+ * A merged value that won't parse falls back to the incoming value alone rather
268
+ * than failing. The row is trying to write something valid, and refusing it over
269
+ * data that was already in the database punishes the wrong write.
270
+ */
271
+ mergeComposite(field, existing, incoming) {
272
+ const merged = custom_field_types_1.FIELD_TYPES[field.type].value.safeParse({ ...existing, ...incoming });
273
+ if (merged.success) {
274
+ return merged.data;
275
+ }
276
+ logging_1.default.warn(`Replacing a custom field value that no longer validates rather than merging into it (field '${field.key}').`);
277
+ return incoming;
278
+ }
191
279
  /**
192
280
  * Apply a plan from `planWrite`.
193
281
  *
194
282
  * Merge, not replace: only the fields in the plan are touched, so a caller
195
283
  * that doesn't know about a field can't erase it.
196
284
  *
285
+ * `mergeComposites` extends that same rule from a field to a composite's
286
+ * sub-fields, because a sub-field is a field: a row writes the sub-fields it
287
+ * fills and leaves the rest alone, exactly as a blank `name` column leaves a
288
+ * member's name alone. The CSV import needs it — without it a file naming one
289
+ * address column would replace the whole address, clearing five sub-fields it
290
+ * never mentioned. The API does not merge: a PUT sends a whole address, so
291
+ * what it sends is what should be stored.
292
+ *
293
+ * The merge is asked for here rather than at plan time, where the caller's intent
294
+ * is otherwise expressed, because it cannot happen before a transaction is open:
295
+ * it reads the stored value and writes it back, and the row lock that keeps those
296
+ * two steps together only exists inside the transaction. `planWrite` runs before
297
+ * one is opened -- the importer plans a row, then opens a transaction per member --
298
+ * so a merge decided there would have to read outside the lock it depends on.
299
+ *
197
300
  * Always transactional, so a mid-batch failure rolls the batch back. Passed an
198
301
  * executor it joins that transaction -- the importer passes its per-member one, so a
199
302
  * failed value write takes the member with it; passed nothing it opens its own.
200
303
  */
201
- async applyWrite(memberId, writes, executor = this.knex) {
304
+ async applyWrite(memberId, writes, executor = this.knex, { mergeComposites = false } = {}) {
202
305
  if (writes.length === 0) {
203
306
  return;
204
307
  }
205
308
  const apply = async (trx) => {
309
+ const stored = mergeComposites
310
+ ? await this.storedComposites(trx, memberId, writes)
311
+ : new Map();
206
312
  for (const { field, value } of writes) {
207
313
  const target = { member_id: memberId, custom_field_id: field.id };
208
314
  if (value === undefined) {
209
315
  await trx(VALUES_TABLE).where(target).del();
210
316
  continue;
211
317
  }
212
- const valueColumns = (0, storage_1.storageColumnsFor)(field.type, value);
318
+ // Both sides of the merge go through the same shape check, so it joins
319
+ // two objects the type system knows about rather than two assertions.
320
+ // The plan already validated `value` against its field type, so for a
321
+ // composite this parse cannot fail; a scalar simply doesn't match, and
322
+ // falls through to being written as it stands.
323
+ const existing = stored.get(field.id);
324
+ const incoming = schema_1.StoredCompositeValue.safeParse(value);
325
+ const merged = existing && incoming.success
326
+ ? this.mergeComposite(field, existing, incoming.data)
327
+ : value;
328
+ const valueColumns = (0, storage_1.storageColumnsFor)(field.type, merged);
213
329
  await trx(VALUES_TABLE)
214
330
  .insert({ id: new bson_objectid_1.default().toHexString(), ...target, ...valueColumns, created_at: new Date() })
215
331
  // The unique index on (member_id, custom_field_id) is
@@ -3,8 +3,8 @@ import errors from '@tryghost/errors';
3
3
  import logging from '@tryghost/logging';
4
4
  import type {Knex} from 'knex';
5
5
  import {z} from 'zod';
6
- import {FIELD_TYPES, type FieldType} from '@tryghost/custom-field-types';
7
- import {DbCustomFieldValueWithField, FIELD_STATUS} from './schema';
6
+ import {FIELD_TYPES, subFieldsOf, type FieldType} from '@tryghost/custom-field-types';
7
+ import {DbCustomFieldValueWithField, FIELD_STATUS, StoredCompositeValue} from './schema';
8
8
  import {activeFields} from './queries';
9
9
  import {storageCodecFor, storageColumnsFor} from './storage';
10
10
 
@@ -20,6 +20,21 @@ const MAX_KEY_LENGTH = 191;
20
20
  // resolved to a definition.
21
21
  const ValuesInput = z.record(z.string().max(MAX_KEY_LENGTH), z.unknown());
22
22
 
23
+ // JSON.parse as a total function. Anything unreadable — unparseable text, or the null
24
+ // a nullable column can always return — reads as `undefined`, which no value schema
25
+ // accepts. That way a corrupt blob is turned away by the same check that turns away a
26
+ // well-formed one of the wrong shape, instead of needing a branch of its own.
27
+ function decodeJson(raw: string | null): unknown {
28
+ if (raw === null) {
29
+ return undefined;
30
+ }
31
+ try {
32
+ return JSON.parse(raw);
33
+ } catch {
34
+ return undefined;
35
+ }
36
+ }
37
+
23
38
  // The field facts the value path needs: the id to write the FK, the key to match
24
39
  // input against, the name for error messages, and the type to pick the validator
25
40
  // and storage column. All columns on members_custom_fields, so a value operation
@@ -222,22 +237,126 @@ export class CustomFieldValuesService {
222
237
  return writes;
223
238
  }
224
239
 
240
+ /**
241
+ * @private
242
+ * The composite values this member already holds for the fields in a plan,
243
+ * keyed by field id. Composites only: a scalar has nothing to merge into.
244
+ *
245
+ * Read through the caller's executor, never the base connection. On a
246
+ * single-connection pool a read off `this.knex` inside an open transaction
247
+ * waits for the connection that transaction is holding, and hangs.
248
+ *
249
+ * A blob that won't decode is left out, so the write replaces it rather than
250
+ * merging into something unreadable.
251
+ */
252
+ private async storedComposites(trx: Knex, memberId: string, writes: PlannedWrite[]): Promise<Map<string, Record<string, unknown>>> {
253
+ const composites = new Map<string, Record<string, unknown>>();
254
+ // Merging is only meaningful for a value with parts, which is a fact about the
255
+ // type's shape rather than the column it lands in. Asking the storage type
256
+ // instead would sweep in the first json-backed type that isn't a record — a
257
+ // multi-select held as a list, say — and log a decode failure for every row of
258
+ // an import, over a value that reads back perfectly well.
259
+ const fieldIds = writes
260
+ .filter(({field, value}) => value !== undefined && subFieldsOf(field.type) !== null)
261
+ .map(({field}) => field.id);
262
+
263
+ if (fieldIds.length === 0) {
264
+ return composites;
265
+ }
266
+
267
+ const rows = await trx(VALUES_TABLE)
268
+ .where('member_id', memberId)
269
+ .whereIn('custom_field_id', fieldIds)
270
+ .whereNotNull('value_json')
271
+ .select('custom_field_id', 'value_json')
272
+ // Merging reads a value and writes it back, so the row has to stay still in
273
+ // between: without the lock a write committing after this read is overwritten
274
+ // by the sub-fields it just replaced. knex omits this on SQLite, which needs
275
+ // no lock — its pool is a single connection, so writes never overlap.
276
+ .forUpdate();
277
+
278
+ for (const row of rows) {
279
+ // The blob is the edge here: the columns come from a query written just
280
+ // above, but whatever an earlier write left in value_json is unknown until
281
+ // it is read back. Parsing it is what narrows the type, and it rejects the
282
+ // array, null or bare number the column could equally be holding.
283
+ const stored = StoredCompositeValue.safeParse(decodeJson(row.value_json));
284
+ if (stored.success) {
285
+ composites.set(row.custom_field_id, stored.data);
286
+ continue;
287
+ }
288
+ logging.warn(`Replacing an unreadable custom field value rather than merging into it (member '${memberId}', field '${row.custom_field_id}').`);
289
+ }
290
+
291
+ return composites;
292
+ }
293
+
294
+ /**
295
+ * @private
296
+ * The stored sub-fields with the incoming ones written over them, re-checked
297
+ * against the field type before it is handed back.
298
+ *
299
+ * The re-check is what makes this safe to write without a second thought. Both
300
+ * halves were valid when they were written, but "valid" is a moving target: the
301
+ * stored half may predate a change to the type, so it can carry a sub-field the
302
+ * type has since dropped or a bound it has since tightened. Parsing the result
303
+ * strips what no longer belongs instead of faithfully persisting it on every
304
+ * import from here on.
305
+ *
306
+ * A merged value that won't parse falls back to the incoming value alone rather
307
+ * than failing. The row is trying to write something valid, and refusing it over
308
+ * data that was already in the database punishes the wrong write.
309
+ */
310
+ private mergeComposite(field: ActiveField, existing: Record<string, unknown>, incoming: Record<string, unknown>): unknown {
311
+ const merged = FIELD_TYPES[field.type].value.safeParse({...existing, ...incoming});
312
+ if (merged.success) {
313
+ return merged.data;
314
+ }
315
+
316
+ logging.warn(`Replacing a custom field value that no longer validates rather than merging into it (field '${field.key}').`);
317
+ return incoming;
318
+ }
319
+
225
320
  /**
226
321
  * Apply a plan from `planWrite`.
227
322
  *
228
323
  * Merge, not replace: only the fields in the plan are touched, so a caller
229
324
  * that doesn't know about a field can't erase it.
230
325
  *
326
+ * `mergeComposites` extends that same rule from a field to a composite's
327
+ * sub-fields, because a sub-field is a field: a row writes the sub-fields it
328
+ * fills and leaves the rest alone, exactly as a blank `name` column leaves a
329
+ * member's name alone. The CSV import needs it — without it a file naming one
330
+ * address column would replace the whole address, clearing five sub-fields it
331
+ * never mentioned. The API does not merge: a PUT sends a whole address, so
332
+ * what it sends is what should be stored.
333
+ *
334
+ * The merge is asked for here rather than at plan time, where the caller's intent
335
+ * is otherwise expressed, because it cannot happen before a transaction is open:
336
+ * it reads the stored value and writes it back, and the row lock that keeps those
337
+ * two steps together only exists inside the transaction. `planWrite` runs before
338
+ * one is opened -- the importer plans a row, then opens a transaction per member --
339
+ * so a merge decided there would have to read outside the lock it depends on.
340
+ *
231
341
  * Always transactional, so a mid-batch failure rolls the batch back. Passed an
232
342
  * executor it joins that transaction -- the importer passes its per-member one, so a
233
343
  * failed value write takes the member with it; passed nothing it opens its own.
234
344
  */
235
- async applyWrite(memberId: string, writes: PlannedWrite[], executor: Knex = this.knex): Promise<void> {
345
+ async applyWrite(
346
+ memberId: string,
347
+ writes: PlannedWrite[],
348
+ executor: Knex = this.knex,
349
+ {mergeComposites = false}: {mergeComposites?: boolean} = {}
350
+ ): Promise<void> {
236
351
  if (writes.length === 0) {
237
352
  return;
238
353
  }
239
354
 
240
355
  const apply = async (trx: Knex) => {
356
+ const stored = mergeComposites
357
+ ? await this.storedComposites(trx, memberId, writes)
358
+ : new Map<string, Record<string, unknown>>();
359
+
241
360
  for (const {field, value} of writes) {
242
361
  const target = {member_id: memberId, custom_field_id: field.id};
243
362
 
@@ -246,7 +365,18 @@ export class CustomFieldValuesService {
246
365
  continue;
247
366
  }
248
367
 
249
- const valueColumns = storageColumnsFor(field.type, value);
368
+ // Both sides of the merge go through the same shape check, so it joins
369
+ // two objects the type system knows about rather than two assertions.
370
+ // The plan already validated `value` against its field type, so for a
371
+ // composite this parse cannot fail; a scalar simply doesn't match, and
372
+ // falls through to being written as it stands.
373
+ const existing = stored.get(field.id);
374
+ const incoming = StoredCompositeValue.safeParse(value);
375
+ const merged = existing && incoming.success
376
+ ? this.mergeComposite(field, existing, incoming.data)
377
+ : value;
378
+
379
+ const valueColumns = storageColumnsFor(field.type, merged);
250
380
  await trx(VALUES_TABLE)
251
381
  .insert({id: new ObjectID().toHexString(), ...target, ...valueColumns, created_at: new Date()})
252
382
  // The unique index on (member_id, custom_field_id) is
@@ -9,6 +9,7 @@ const charset = require('charset');
9
9
  const iconv = require('iconv-lite');
10
10
  const path = require('path');
11
11
  const crypto = require('crypto');
12
+ const imageTransform = require('@tryghost/image-transform');
12
13
 
13
14
  // Some sites block non-standard user agents so we need to mimic a typical browser
14
15
  // Note: the Ghost/5.0 string _may_ be in use by 3rd parties so use caution when updating across majors
@@ -39,7 +40,26 @@ const messages = {
39
40
  insufficientMetadata: 'URL contains insufficient metadata.',
40
41
  unknownProvider: 'No provider found for supplied URL.',
41
42
  unableToFetchOembed: 'Unable to fetch requested embed.',
42
- unauthorized: 'URL contains a private resource.'
43
+ unauthorized: 'URL contains a private resource.',
44
+ unconvertibleSvg: 'SVG image is too large or compressed to convert.'
45
+ };
46
+
47
+ const SVG_RASTER_SIZE = 256;
48
+ const SVG_RASTER_TIMEOUT_SECONDS = 10;
49
+ const MAX_SVG_BYTES = 32 * 1024;
50
+ const GZIP_MAGIC = [0x1f, 0x8b];
51
+
52
+ const SVG_EXTENSIONS = new Set(['.svg', '.svgz']);
53
+ const SVG_SNIFF_BYTES = 1024;
54
+
55
+ const shouldRasterize = (buffer, ext) => {
56
+ if (SVG_EXTENSIONS.has(ext.toLowerCase())) {
57
+ return true;
58
+ }
59
+
60
+ const head = buffer.subarray(0, SVG_SNIFF_BYTES).toString('utf8').trimStart();
61
+
62
+ return head.startsWith('<') && /<svg[\s:>]/i.test(head);
43
63
  };
44
64
 
45
65
  /**
@@ -175,14 +195,35 @@ class OEmbedService {
175
195
  }
176
196
 
177
197
  // Fetch image buffer from the URL
178
- const imageBuffer = await this.fetchImageBuffer(imageUrl);
198
+ let imageBuffer = await this.fetchImageBuffer(imageUrl);
179
199
 
180
200
  // Extract file name from URL
181
201
  const fileName = path.basename(new URL(imageUrl).pathname);
182
- const ext = path.extname(fileName);
202
+ let ext = path.extname(fileName);
183
203
  const baseName = ext ? path.basename(fileName, ext) : fileName;
184
204
  const name = this.imageStore.getSanitizedFileName(baseName);
185
205
 
206
+ if (shouldRasterize(imageBuffer, ext)) {
207
+ if (imageBuffer.length > MAX_SVG_BYTES || GZIP_MAGIC.every((byte, index) => imageBuffer[index] === byte)) {
208
+ throw new errors.ValidationError({
209
+ message: tpl(messages.unconvertibleSvg),
210
+ context: imageUrl
211
+ });
212
+ }
213
+
214
+ imageBuffer = await imageTransform.resizeFromBuffer(imageBuffer, {
215
+ // without `format` this returns the original bytes whenever they
216
+ // are smaller, storing the SVG under a .png name
217
+ format: 'png',
218
+ width: SVG_RASTER_SIZE,
219
+ height: SVG_RASTER_SIZE,
220
+ withoutEnlargement: false,
221
+ timeout: SVG_RASTER_TIMEOUT_SECONDS
222
+ });
223
+
224
+ ext = '.png';
225
+ }
226
+
186
227
  const uniqueFileName = `${name}-${crypto.randomUUID()}${ext}`;
187
228
  const targetPath = path.join(imageType, uniqueFileName);
188
229
 
@@ -312,6 +312,17 @@ class PostsService {
312
312
  });
313
313
  }
314
314
 
315
+ // The `comments.in_reply_to_id` references form chains between a post's
316
+ // comments, which MySQL cannot resolve while cascade-deleting them
317
+ // alongside `comments.parent_id`. Clear the references first so the
318
+ // `comments.post_id` cascade delete can do its job
319
+ await this.models.Post.bulkEdit(deleteIds, 'comments', {
320
+ data: {in_reply_to_id: null},
321
+ column: 'post_id',
322
+ transacting: options.transacting,
323
+ throwErrors: true
324
+ });
325
+
315
326
  // Posts and emails
316
327
  await this.models.Post.bulkDestroy(deleteEmailIds, 'emails', {transacting: options.transacting, throwErrors: true});
317
328
  const result = await this.models.Post.bulkDestroy(deleteIds, 'posts', {...options, throwErrors: true});
@@ -54,10 +54,8 @@ class DynamicRoutingService {
54
54
  }
55
55
 
56
56
  async loadRouteSettings() {
57
- const {buildRouterSettings} = require('./activation-bridge');
58
-
59
57
  try {
60
- return buildRouterSettings(await this.store.get());
58
+ return await this.store.get();
61
59
  } catch (err) {
62
60
  // A stored-content error means the site's routes.yaml is invalid —
63
61
  // either it fails validation or it isn't parseable YAML. Log a