ghost 6.55.0 → 6.57.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 (483) hide show
  1. package/bin/validate-adapters.js +74 -0
  2. package/bin/validate-adapters.ts +87 -0
  3. package/bin/warm-compile-cache.sh +60 -0
  4. package/components/tryghost-custom-field-types-0.0.0.tgz +0 -0
  5. package/components/tryghost-kg-default-cards-10.3.3.tgz +0 -0
  6. package/components/tryghost-kg-default-nodes-2.1.4.tgz +0 -0
  7. package/components/tryghost-kg-default-transforms-1.3.3.tgz +0 -0
  8. package/components/tryghost-kg-html-to-lexical-1.3.3.tgz +0 -0
  9. package/components/tryghost-kg-lexical-html-renderer-1.4.3.tgz +0 -0
  10. package/components/tryghost-nql-string-0.0.0.tgz +0 -0
  11. package/components/tryghost-parse-email-address-0.0.0.tgz +0 -0
  12. package/core/app.js +1 -1
  13. package/core/boot.js +21 -25
  14. package/core/bridge.js +10 -12
  15. package/core/built/admin/assets/Combination-BDvCdg1t.js +41 -0
  16. package/core/built/admin/assets/{PolarAngleAxis-BGjf7sBu.js → PolarAngleAxis-C4qtHy1B.js} +1 -1
  17. package/core/built/admin/assets/{RadialBarChart-zeFf9mBz.js → RadialBarChart-Bh09Stsj.js} +1 -1
  18. package/core/built/admin/assets/about-C7O-AgRZ.js +1 -0
  19. package/core/built/admin/assets/{account-migration-sjHLx5dy.js → account-migration-Cv_ol1ko.js} +1 -1
  20. package/core/built/admin/assets/action-list-lGzZLHc8.js +1 -0
  21. package/core/built/admin/assets/activitypub/{account-migration-DoYXtnZ8.js → account-migration-DHFbePl6.js} +7 -7
  22. package/core/built/admin/assets/activitypub/activitypub.js +6 -6
  23. package/core/built/admin/assets/activitypub/{avatar-B3DEDB1l.js → avatar-CT4tS2_S.js} +2 -2
  24. package/core/built/admin/assets/activitypub/{avatar-flipboard-Cqrg0XMx.js → avatar-flipboard-ByYWKipo.js} +2 -2
  25. package/core/built/admin/assets/activitypub/{bluesky-sharing-Bxeh6MKW.js → bluesky-sharing-srVnSHDz.js} +16 -16
  26. package/core/built/admin/assets/activitypub/{content-formatters-BpQARh28.js → content-formatters-C2sos5Ht.js} +3 -3
  27. package/core/built/admin/assets/activitypub/{copy-D9UWIAan.js → copy-B1UBTOoZ.js} +2 -2
  28. package/core/built/admin/assets/activitypub/{dist-D8sFfdpC.js → dist-MPsb2WZ1.js} +12 -12
  29. package/core/built/admin/assets/activitypub/{domain-CWueFW6q.js → domain-DzcM9Dzo.js} +9 -9
  30. package/core/built/admin/assets/activitypub/{edit-profile-BGxPU7N5.js → edit-profile-CsQ_4xAk.js} +945 -945
  31. package/core/built/admin/assets/activitypub/{explore-DBK-u0Ch.js → explore-CJe0DZBK.js} +8 -8
  32. package/core/built/admin/assets/activitypub/{feed-Bcir5RQZ.js → feed-CKV3qWON.js} +33 -33
  33. package/core/built/admin/assets/activitypub/{field-BV-CIT3d.js → field-DckHfDJf.js} +3 -3
  34. package/core/built/admin/assets/activitypub/{hash-BC9Q0N4c.js → hash-Dlp8RM55.js} +2 -2
  35. package/core/built/admin/assets/activitypub/{inbox-DQ4ZnC1m.js → inbox-CsPy4R0M.js} +2 -2
  36. package/core/built/admin/assets/activitypub/{inbox-yC0nfvzl.js → inbox-DAsjzUUe.js} +85 -85
  37. package/core/built/admin/assets/activitypub/{label-DTRS9TDk.js → label-DWyo9E5T.js} +3 -3
  38. package/core/built/admin/assets/activitypub/layout-DM_3pqsU.js +1 -0
  39. package/core/built/admin/assets/activitypub/{moderation-Dwl9t0pb.js → moderation-B0Inc03M.js} +11 -11
  40. package/core/built/admin/assets/activitypub/{note-B-zKsZ7r.js → note-CSE54lER.js} +7 -7
  41. package/core/built/admin/assets/activitypub/{notifications-DjJZD54t.js → notifications-RSwbKqdh.js} +9 -9
  42. package/core/built/admin/assets/activitypub/{preferences-CoU8vs6d.js → preferences-CE231n2_.js} +20 -20
  43. package/core/built/admin/assets/activitypub/{profile-BAsaJekx.js → profile-aaQ1j1Eo.js} +237 -237
  44. package/core/built/admin/assets/activitypub/{reply-CpdMeAO5.js → reply-Cjb0Vs3m.js} +2 -2
  45. package/core/built/admin/assets/activitypub/{routes-CLH3OVTP.js → routes-Djb0mc9k.js} +1139 -1150
  46. package/core/built/admin/assets/activitypub/{separator-BI1FLsYM.js → separator-D8pROI5n.js} +3 -3
  47. package/core/built/admin/assets/activitypub/{settings-DA6pNJ-Q.js → settings-DMc32IQl.js} +13 -13
  48. package/core/built/admin/assets/activitypub/{site-CQD42Pw5.js → site-DVIcsohX.js} +81 -81
  49. package/core/built/admin/assets/activitypub/{step-1-DIDg4aHb.js → step-1-9Yey9usF.js} +5 -5
  50. package/core/built/admin/assets/activitypub/{step-2-BfGe8BYt.js → step-2-DdYmKRXV.js} +6 -6
  51. package/core/built/admin/assets/activitypub/{step-3-mE9spAqw.js → step-3-hdSkcjZU.js} +10 -10
  52. package/core/built/admin/assets/activitypub/{tabs-Cg0saow1.js → tabs-CUqseYzQ.js} +317 -315
  53. package/core/built/admin/assets/activitypub/{topic-filter-DLHBZT93.js → topic-filter-Coni6OXM.js} +3 -3
  54. package/core/built/admin/assets/activitypub/{use-activity-pub-queries-VzbN99u_.js → use-activity-pub-queries-MeHTPMQv.js} +2 -2
  55. package/core/built/admin/assets/activitypub/{use-navigate-with-base-path-CAwXTmqH.js → use-navigate-with-base-path-B046AM38.js} +115 -96
  56. package/core/built/admin/assets/activitypub/{use-reply-chain-data-Br4aOWOh.js → use-reply-chain-data-1AO-Xob6.js} +5 -5
  57. package/core/built/admin/assets/activitypub/{x-C8tAnXly.js → x-BRfRYd16.js} +2 -2
  58. package/core/built/admin/assets/add-integration-modal-m9Dv6f77.js +1 -0
  59. package/core/built/admin/assets/add-newsletter-modal-CPSVZL2H.js +1 -0
  60. package/core/built/admin/assets/add-recommendation-modal-Ci9Z5yat.js +1 -0
  61. package/core/built/admin/assets/analytics-DqSCVV8U.js +1 -0
  62. package/core/built/admin/assets/announcement-bar-modal-DU3avV7d.js +1 -0
  63. package/core/built/admin/assets/api-keys-BK0LylyK.js +1 -0
  64. package/core/built/admin/assets/api-keys-_iCT4dxr.js +1 -0
  65. package/core/built/admin/assets/{audience-DjEWNhp3.js → audience-DTJtXXWY.js} +1 -1
  66. package/core/built/admin/assets/automated-emails-PayPCT8L.js +1 -0
  67. package/core/built/admin/assets/{automations-DhKeS_U5.js → automations-162q5HjL.js} +1 -1
  68. package/core/built/admin/assets/{automations-Bx3VcFIL.js → automations-dtQ_MRZ7.js} +1 -1
  69. package/core/built/admin/assets/{bluesky-sharing-ClMd0-Ft.js → bluesky-sharing-B8Es3AuV.js} +1 -1
  70. package/core/built/admin/assets/{box-BWbhXh21.js → box-By_EgGtm.js} +1 -1
  71. package/core/built/admin/assets/brand-icon-09YqFZjV.js +1 -0
  72. package/core/built/admin/assets/{breadcrumb-BD0U2ygg.js → breadcrumb-tg5zL--z.js} +1 -1
  73. package/core/built/admin/assets/build-CsiH26uS.js +1 -0
  74. package/core/built/admin/assets/{card-DGoDJzrn.js → card-aUHX0hyR.js} +1 -1
  75. package/core/built/admin/assets/{chart-BuRT2Y6p.js → chart-BnXyfH11.js} +1 -1
  76. package/core/built/admin/assets/{chart-helpers-fjQKRVyY.js → chart-helpers-CZElTGYL.js} +1 -1
  77. package/core/built/admin/assets/checkbox-Cyo-x7mC.js +1 -0
  78. package/core/built/admin/assets/chunk.524.cd5498dd4e195f679266.js +25 -0
  79. package/core/built/admin/assets/chunk.582.6f92ec1f03734bf2ea3f.js +27 -0
  80. package/core/built/admin/assets/{chunk.481.6c7395a49484bb4583aa.js → chunk.873.b8fc05a8ab877fb819ed.js} +62 -60
  81. package/core/built/admin/assets/code-editor-view-Dq1rQ4tS.js +1 -0
  82. package/core/built/admin/assets/color-picker-field-BonBgxhU.js +1 -0
  83. package/core/built/admin/assets/color-utils-BsrwXuDz.js +1 -0
  84. package/core/built/admin/assets/combobox-uX5r_paI.js +1 -0
  85. package/core/built/admin/assets/command-DxwBO2pF.js +1 -0
  86. package/core/built/admin/assets/comments-Dn-RO-nU.js +1 -0
  87. package/core/built/admin/assets/components-CwL6a6As-B0E0-NBJ.js +1 -0
  88. package/core/built/admin/assets/confirmation-modal-B6D6mk6R.js +1 -0
  89. package/core/built/admin/assets/{container-BYz2Ed39.js → container-LiZf3XdY.js} +1 -1
  90. package/core/built/admin/assets/content-api-modal-BxQ-7T6u.js +1 -0
  91. package/core/built/admin/assets/copy-field-BAxL4Zqn.js +1 -0
  92. package/core/built/admin/assets/currency-Dy4Tc2R2.js +1 -0
  93. package/core/built/admin/assets/custom-integration-modal---d_l7lc.js +1 -0
  94. package/core/built/admin/assets/{data-list-16rkJYYi.js → data-list-CupF7bjM.js} +1 -1
  95. package/core/built/admin/assets/{date-range-select-BFp58EMk.js → date-range-select-PB2n1HZf.js} +1 -1
  96. package/core/built/admin/assets/debounce-Dg2fW5Rg.js +3 -0
  97. package/core/built/admin/assets/design-and-theme-modal-DYRT207a.js +23 -0
  98. package/core/built/admin/assets/dirty-confirm-dialog-BnDJ39ao.js +1 -0
  99. package/core/built/admin/assets/disabled-sources-indicator-iMHrkyV3.js +1 -0
  100. package/core/built/admin/assets/dist-B2PvRwLn.js +1 -0
  101. package/core/built/admin/assets/{dist-DGzPJ_bO.js → dist-Cb5RSHSg.js} +1 -1
  102. package/core/built/admin/assets/{dist-P_ohUlTi.js → dist-CqO2lWS02.js} +1 -1
  103. package/core/built/admin/assets/{dist-pHiOxyxa.js → dist-D5vhMdXf.js} +1 -1
  104. package/core/built/admin/assets/dist-DlCyLrOC.js +1 -0
  105. package/core/built/admin/assets/dist-WeOIvSYM.js +23 -0
  106. package/core/built/admin/assets/{dist-DyViJwIy.js → dist-cFzKLG7y.js} +3 -3
  107. package/core/built/admin/assets/dist-j1xxxnY8.js +1 -0
  108. package/core/built/admin/assets/dist-myQaY0ka.js +1 -0
  109. package/core/built/admin/assets/domain-BR8FIdjD.js +1 -0
  110. package/core/built/admin/assets/{dropzone-DlK-K2DQ.js → dropzone-hNxmUvwD.js} +1 -1
  111. package/core/built/admin/assets/{edit-profile-BKT-REJw.js → edit-profile-CENrkY0l.js} +1 -1
  112. package/core/built/admin/assets/editor-B18D0lm8.js +8 -0
  113. package/core/built/admin/assets/embed-signup-form-modal-BsCYUMvQ.js +9 -0
  114. package/core/built/admin/assets/{empty-indicator-oX1u1Pkn.js → empty-indicator-WB1WgT9h.js} +1 -1
  115. package/core/built/admin/assets/{explore-ClUpJZOp.js → explore-BmrnljWw.js} +1 -1
  116. package/core/built/admin/assets/{feed-DF92dUMP.js → feed-Dy4li6i8.js} +1 -1
  117. package/core/built/admin/assets/{field-Do7lieBA.js → field-Dh_lwk13.js} +1 -1
  118. package/core/built/admin/assets/filter-sources-D2zDuJH5.js +1 -0
  119. package/core/built/admin/assets/filters-3JBQcNtJ.js +1 -0
  120. package/core/built/admin/assets/filters-CFt_BDe4.js +1 -0
  121. package/core/built/admin/assets/first-promoter-modal-D6Rjxugw.js +1 -0
  122. package/core/built/admin/assets/format-url-C-t8G6Qd.js +1 -0
  123. package/core/built/admin/assets/{ghost-a9c788f6f0b1ea6cf0f14ca4b13f4496.css → ghost-d38c30b866e59f2e06a357b5eb844fe5.css} +1 -1
  124. package/core/built/admin/assets/{ghost-c4057474943effc568c5659ccc4909d7.js → ghost-d683fbeb945b13155ea669caa5d8db41.js} +420 -398
  125. package/core/built/admin/assets/{ghost-dark-db36d795d21c61426837df40af270966.css → ghost-dark-1bffd5099ba7a749303133efacf8480b.css} +1 -1
  126. package/core/built/admin/assets/{gift-link-modal-f9VIMNDv.js → gift-link-modal-zMtlJhLQ.js} +1 -1
  127. package/core/built/admin/assets/{gift-links-CsUhefWG.js → gift-links-CVuqLhsZ.js} +1 -1
  128. package/core/built/admin/assets/global-data-provider-B949LDXJ.js +1 -0
  129. package/core/built/admin/assets/grid-CUciBfjf.js +1 -0
  130. package/core/built/admin/assets/{growth-CVw1My4S.js → growth-C38Xtp8D.js} +2 -2
  131. package/core/built/admin/assets/{growth-B3I6gNLm.js → growth-CFws67_i.js} +1 -1
  132. package/core/built/admin/assets/history-modal-ILAOkbxH.js +1 -0
  133. package/core/built/admin/assets/{hooks-CixO01lL.js → hooks-D6EE7BIt.js} +2 -2
  134. package/core/built/admin/assets/html-field-9a9Id_2q.js +1 -0
  135. package/core/built/admin/assets/iframe-buffering-e8vH4-Qn.js +1 -0
  136. package/core/built/admin/assets/images-CrQKVc9Q.js +1 -0
  137. package/core/built/admin/assets/{inbox-CFBSu4Ed.js → inbox-BrSRzyvz.js} +6 -6
  138. package/core/built/admin/assets/index-BHy2DZX-.js +67 -0
  139. package/core/built/admin/assets/index-Ds2iUoF6.css +1 -0
  140. package/core/built/admin/assets/index.es-B9tvf-t4.js +1 -0
  141. package/core/built/admin/assets/{inline-EMAm3okk.js → inline-Cw-gigDW.js} +1 -1
  142. package/core/built/admin/assets/input-group-6vEPvX9S.js +1 -0
  143. package/core/built/admin/assets/{input-surface-BpAjZXY5.js → input-surface-BdOe4KzO.js} +1 -1
  144. package/core/built/admin/assets/integration-header-B4DyXV06.js +1 -0
  145. package/core/built/admin/assets/integrations-BT6_iJvu.js +1 -0
  146. package/core/built/admin/assets/invite-user-modal-5xDCEw90.js +1 -0
  147. package/core/built/admin/assets/isEmail-BOfC647y.js +1 -0
  148. package/core/built/admin/assets/koenig-editor-base-BWLEB9Vf.js +1 -0
  149. package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +9535 -9527
  150. package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +124 -124
  151. package/core/built/admin/assets/{koenig-lexical-f8vHRYvz.js → koenig-lexical-Cj62r_y9.js} +86 -86
  152. package/core/built/admin/assets/{kpi-card-DSap9rAy.js → kpi-card-DOUnkZcN.js} +1 -1
  153. package/core/built/admin/assets/{kpi-card-DU0rn9Cj.js → kpi-card-IHDo8Q56.js} +1 -1
  154. package/core/built/admin/assets/{kpi-helpers-Dz33AV6h.js → kpi-helpers-CjUdECAK.js} +1 -1
  155. package/core/built/admin/assets/{kpi-tabs-BtTTrKvJ.js → kpi-tabs-X3DkOYbI.js} +1 -1
  156. package/core/built/admin/assets/{label-DS8Mi6G5.js → label-Dq6QaOuS.js} +1 -1
  157. package/core/built/admin/assets/label-picker-8YlhS2U3.js +1 -0
  158. package/core/built/admin/assets/labels-DAPjKPy2.js +1 -0
  159. package/core/built/admin/assets/layout-TMj4P-vv.js +1 -0
  160. package/core/built/admin/assets/limit-modal-BRPsit_B.js +1 -0
  161. package/core/built/admin/assets/link-helpers-Bzkk68GD.js +1 -0
  162. package/core/built/admin/assets/{list-page-B6ANtzkd.js → list-page-CdsnQR9N.js} +1 -1
  163. package/core/built/admin/assets/member-custom-fields-CmvsuuV_.js +1 -0
  164. package/core/built/admin/assets/member-detail-DCvRrkmJ.js +1 -0
  165. package/core/built/admin/assets/members-DxbSKngD.js +11 -0
  166. package/core/built/admin/assets/modal-page-BMTzFHVO.js +1 -0
  167. package/core/built/admin/assets/{moderation-DySbgyUS.js → moderation-Da5lhhWh.js} +1 -1
  168. package/core/built/admin/assets/multi-select-combobox-CYdkH7KQ.js +1 -0
  169. package/core/built/admin/assets/navigation-modal-BIoTwtMy.js +1 -0
  170. package/core/built/admin/assets/newsletter-BptlVoJm.js +1 -0
  171. package/core/built/admin/assets/newsletter-detail-modal-BIvxqE0N.js +1 -0
  172. package/core/built/admin/assets/newsletter-emails-Bf8IZLsP.js +1 -0
  173. package/core/built/admin/assets/newsletters-BgrIZk0V.js +1 -0
  174. package/core/built/admin/assets/{newsletters-BXJbJgA-.js → newsletters-Du0lzskS.js} +1 -1
  175. package/core/built/admin/assets/{note-C8kxrz6w.js → note-DYcLOmDb.js} +1 -1
  176. package/core/built/admin/assets/{notifications-CgtSfX7t.js → notifications-B2SPFIV9.js} +1 -1
  177. package/core/built/admin/assets/{offers-DEniVRD5.js → offers-Um6ab71O.js} +1 -1
  178. package/core/built/admin/assets/offers-container-modal-BZnZITMw.js +1 -0
  179. package/core/built/admin/assets/{onboarding-route-4qtziA9d.js → onboarding-route-DgdfxkQh.js} +1 -1
  180. package/core/built/admin/assets/overview-BYQJXSog.js +1 -0
  181. package/core/built/admin/assets/overview-Dt2_mR6U.js +1 -0
  182. package/core/built/admin/assets/{page-header-B3Wb9Jqv.js → page-header-CpmxJj5B.js} +1 -1
  183. package/core/built/admin/assets/{pages-B4goRPUn.js → pages-BVMnpI2O.js} +1 -1
  184. package/core/built/admin/assets/pintura-modal-B4MuBuuO.js +1 -0
  185. package/core/built/admin/assets/portal-frame-BF0NBIji.js +1 -0
  186. package/core/built/admin/assets/portal-modal-DMVbm0oy.js +1 -0
  187. package/core/built/admin/assets/{post-analytics-We-ko5Cb.js → post-analytics-CXWBOGyP.js} +1 -1
  188. package/core/built/admin/assets/post-analytics-header-ChzjXVPt.js +1 -0
  189. package/core/built/admin/assets/{post-analytics-provider-B6UynTGb.js → post-analytics-provider-Cgw4U_fw.js} +1 -1
  190. package/core/built/admin/assets/{post-share-modal-DFfO93_0.js → post-share-modal-B9Vmk_XM.js} +1 -1
  191. package/core/built/admin/assets/{posts-DhXE7b6r.js → posts-CxG7v0-M.js} +1 -1
  192. package/core/built/admin/assets/preferences-BfzJmnsd.js +5 -0
  193. package/core/built/admin/assets/preview-chrome-BOVO6_PE.js +1 -0
  194. package/core/built/admin/assets/preview-modal-Dw_HtD6S.js +1 -0
  195. package/core/built/admin/assets/profile-BT5vtv9b.js +1 -0
  196. package/core/built/admin/assets/radio-group-I7ou7PoJ.js +1 -0
  197. package/core/built/admin/assets/{referrers-pmJT4k3-.js → referrers-B9QDAfYm.js} +1 -1
  198. package/core/built/admin/assets/{router-provider-C1R4AZ2o.js → router-provider-DSQfprxH.js} +1 -1
  199. package/core/built/admin/assets/select-D9oU1IoM.js +1 -0
  200. package/core/built/admin/assets/settings-CfZFbyu_.js +4 -0
  201. package/core/built/admin/assets/{settings-WXNeWyU1.js → settings-CpvHWix7.js} +1 -1
  202. package/core/built/admin/assets/settings-breadcrumbs-K0USP2nw.js +1 -0
  203. package/core/built/admin/assets/{shade-app-B3heOAfz-CGXQwWeO.js → shade-app-B3heOAfz-BhQXxNkT.js} +1 -1
  204. package/core/built/admin/assets/{share-modal-DsgW3wbg.js → share-modal-ClIyPzc5.js} +1 -1
  205. package/core/built/admin/assets/slack-modal-1XXqOfiw.js +1 -0
  206. package/core/built/admin/assets/social-urls-CHQ9cTAK.js +1 -0
  207. package/core/built/admin/assets/{sort-button-Bb_FCl7e.js → sort-button-CdBsm40v.js} +1 -1
  208. package/core/built/admin/assets/{source-icon-BeaRxUn9.js → source-icon-CEYmVLXR.js} +1 -1
  209. package/core/built/admin/assets/{stack-xiN0eQ6Z.js → stack-CIfOAAVf.js} +1 -1
  210. package/core/built/admin/assets/{stats-Bjj_kmJ1.js → stats-D8nZCTy5.js} +1 -1
  211. package/core/built/admin/assets/{stats-view-BxPbW6K1.js → stats-view-CugFVf0g.js} +1 -1
  212. package/core/built/admin/assets/{step-1-B1e3-1F8.js → step-1-DOYjrORn.js} +1 -1
  213. package/core/built/admin/assets/{step-2-C5WaeBJo.js → step-2-vA6RsarH.js} +1 -1
  214. package/core/built/admin/assets/{step-3-B46iVPwu.js → step-3-BtIBxMId.js} +2 -2
  215. package/core/built/admin/assets/stripe-button-UFP4QRXd.js +1 -0
  216. package/core/built/admin/assets/stripe-connect-modal-C_L3uM6n.js +1 -0
  217. package/core/built/admin/assets/switch-CE2NuMn-.js +1 -0
  218. package/core/built/admin/assets/{table-CB-4_HBJ.js → table-TMCgCQK7.js} +1 -1
  219. package/core/built/admin/assets/tabs-Dd6oOKbv.js +1 -0
  220. package/core/built/admin/assets/tag-detail-D3l-UJPR.js +1 -0
  221. package/core/built/admin/assets/tags-BwCgmr5v.js +1 -0
  222. package/core/built/admin/assets/tags-WdKQGWR8.js +1 -0
  223. package/core/built/admin/assets/testimonials-modal-CqWp-xXJ.js +1 -0
  224. package/core/built/admin/assets/{text-DwtjwGkh.js → text-DU9Hv33_.js} +1 -1
  225. package/core/built/admin/assets/textarea-d9qEkMOP.js +1 -0
  226. package/core/built/admin/assets/tier-detail-modal-BkcR2u0A.js +1 -0
  227. package/core/built/admin/assets/{tiers-DYsa4DCo.js → tiers-Da6kxHQP.js} +1 -1
  228. package/core/built/admin/assets/toggle-group-Guorw93z.js +1 -0
  229. package/core/built/admin/assets/{topic-filter-DAFT87tQ.js → topic-filter-CbgoWIHn.js} +1 -1
  230. package/core/built/admin/assets/transistor-modal-DuNnnpal.js +1 -0
  231. package/core/built/admin/assets/unsplash-modal-y2bVwTHH.js +1 -0
  232. package/core/built/admin/assets/{use-analytics-data-S0FTd43s.js → use-analytics-data-B-b4wNSC.js} +1 -1
  233. package/core/built/admin/assets/use-check-theme-limit-error-m09J38Mq.js +2 -0
  234. package/core/built/admin/assets/use-feature-flag-ud0Qz8vt.js +1 -0
  235. package/core/built/admin/assets/{use-filter-params-Gpoq2E1L.js → use-filter-params-BnbBDc7T.js} +1 -1
  236. package/core/built/admin/assets/use-filterable-api-iMpDZBxD.js +1 -0
  237. package/core/built/admin/assets/use-form-vRjI0Kwk.js +1 -0
  238. package/core/built/admin/assets/{use-growth-stats-CIaiHOVb.js → use-growth-stats-DPU5rt3W.js} +1 -1
  239. package/core/built/admin/assets/use-hash-link-navigation-guard-B9j6HIrV.js +1 -0
  240. package/core/built/admin/assets/use-limiter-BkN3E4gw.js +2 -0
  241. package/core/built/admin/assets/use-pintura-config-C_z8HTG2.js +1 -0
  242. package/core/built/admin/assets/use-pintura-editor-DNgbkhGp.js +1 -0
  243. package/core/built/admin/assets/{use-post-referrers-Cq2vBPrA.js → use-post-referrers-C2mXyrp-.js} +1 -1
  244. package/core/built/admin/assets/use-query-params-DHhhu8wi.js +1 -0
  245. package/core/built/admin/assets/{use-reply-chain-data-DBDRtGu-.js → use-reply-chain-data-DaK8q2qQ.js} +1 -1
  246. package/core/built/admin/assets/use-setting-group-YlYcoNyP.js +1 -0
  247. package/core/built/admin/assets/use-settings-navigation-Bf1g1rEh.js +1 -0
  248. package/core/built/admin/assets/{use-simple-pagination-CjYNxdwe.js → use-simple-pagination-D9ckKo5-.js} +1 -1
  249. package/core/built/admin/assets/{use-tinybird-query-falHAtuK.js → use-tinybird-query-GSa32hVw.js} +1 -1
  250. package/core/built/admin/assets/{use-tinybird-token-D7-8ND1y.js → use-tinybird-token-CKLOZFsw.js} +1 -1
  251. package/core/built/admin/assets/use-url-input-z-p0Kewm.js +5 -0
  252. package/core/built/admin/assets/user-detail-modal-BMxOA4Sv.js +1 -0
  253. package/core/built/admin/assets/{users-Dkv8NQZe.js → users-B6hIds6n.js} +1 -1
  254. package/core/built/admin/assets/validator-Cr0sMKuW.js +7 -0
  255. package/core/built/admin/assets/{virtual-list-CvnC9zF2.js → virtual-list-RHeEL05L.js} +1 -1
  256. package/core/built/admin/assets/{web-jFYWCKDn.js → web-Bw2QMO5u.js} +2 -2
  257. package/core/built/admin/assets/web-xJtcwMy4.js +1 -0
  258. package/core/built/admin/assets/zapier-modal-BP2f4-vY.js +1 -0
  259. package/core/built/admin/index.html +20 -18
  260. package/core/frontend/helpers/authors.js +1 -1
  261. package/core/frontend/helpers/link.js +3 -8
  262. package/core/frontend/helpers/tags.js +1 -1
  263. package/core/frontend/meta/author-url.js +2 -2
  264. package/core/frontend/meta/url.js +4 -4
  265. package/core/frontend/services/data/fetch-data.js +10 -5
  266. package/core/frontend/services/proxy.js +1 -1
  267. package/core/frontend/services/routing/api-adapter.js +115 -0
  268. package/core/frontend/services/routing/api-adapter.ts +157 -0
  269. package/core/frontend/services/routing/collection-router.js +5 -3
  270. package/core/frontend/services/routing/controllers/index.js +1 -1
  271. package/core/frontend/services/routing/controllers/rss.js +26 -31
  272. package/core/frontend/services/routing/controllers/rss.ts +66 -0
  273. package/core/frontend/services/routing/controllers/static.js +8 -7
  274. package/core/frontend/services/routing/events.js +20 -6
  275. package/core/frontend/services/routing/parent-router.js +17 -4
  276. package/core/frontend/services/routing/permalink-adapter.js +13 -0
  277. package/core/frontend/services/routing/permalink-adapter.ts +10 -0
  278. package/core/frontend/services/routing/registry.js +59 -33
  279. package/core/frontend/services/routing/router-manager.js +24 -22
  280. package/core/frontend/services/routing/static-routes-router.js +4 -4
  281. package/core/frontend/services/routing/taxonomy-router.js +6 -2
  282. package/core/frontend/services/sitemap/base-site-map-generator.js +1 -28
  283. package/core/frontend/services/sitemap/site-map-manager.js +22 -57
  284. package/core/server/adapters/route-settings/utils.js +10 -2
  285. package/core/server/adapters/route-settings/utils.ts +12 -2
  286. package/core/server/api/endpoints/email-post.js +1 -1
  287. package/core/server/api/endpoints/previews.js +1 -1
  288. package/core/server/api/endpoints/search-index-public.js +3 -3
  289. package/core/server/api/endpoints/search-index.js +4 -4
  290. package/core/server/api/endpoints/utils/serializers/input/authors.js +2 -2
  291. package/core/server/api/endpoints/utils/serializers/input/comments.js +1 -1
  292. package/core/server/api/endpoints/utils/serializers/input/pages.js +3 -3
  293. package/core/server/api/endpoints/utils/serializers/input/posts.js +3 -3
  294. package/core/server/api/endpoints/utils/serializers/input/tags.js +1 -1
  295. package/core/server/api/endpoints/utils/serializers/input/users.js +2 -2
  296. package/core/server/api/endpoints/utils/serializers/input/utils/url.js +31 -13
  297. package/core/server/api/endpoints/utils/serializers/output/mappers/posts.js +2 -2
  298. package/core/server/api/endpoints/utils/serializers/output/mappers/tags.js +2 -2
  299. package/core/server/api/endpoints/utils/serializers/output/mappers/users.js +8 -0
  300. package/core/server/api/endpoints/utils/serializers/output/utils/url.js +18 -11
  301. 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
  302. package/core/server/data/migrations/versions/6.56/2026-08-04-14-42-39-restrict-notification-add-permission-to-administrators.js +12 -0
  303. package/core/server/data/migrations/versions/6.57/2026-08-04-18-00-00-store-custom-field-values-as-leaf-rows.js +74 -0
  304. package/core/server/data/migrations/versions/6.57/2026-08-04-18-54-14-reset-automation-email-analytics.js +25 -0
  305. package/core/server/data/schema/commands.js +21 -5
  306. package/core/server/data/schema/fixtures/fixtures.json +2 -2
  307. package/core/server/data/schema/schema.js +25 -5
  308. package/core/server/lib/common/to-plain.ts +1 -1
  309. package/core/server/models/post.js +12 -3
  310. package/core/server/services/adapter-manager/adapter-manager.js +1 -45
  311. package/core/server/services/adapter-manager/adapter-manager.ts +2 -48
  312. package/core/server/services/adapter-manager/adapter-paths.js +24 -0
  313. package/core/server/services/adapter-manager/adapter-paths.ts +21 -0
  314. package/core/server/services/adapter-manager/base-classes.js +22 -0
  315. package/core/server/services/adapter-manager/base-classes.ts +22 -0
  316. package/core/server/services/adapter-manager/index.js +4 -27
  317. package/core/server/services/adapter-manager/index.ts +4 -31
  318. package/core/server/services/adapter-manager/utils.js +50 -0
  319. package/core/server/services/adapter-manager/utils.ts +57 -0
  320. package/core/server/services/audience-feedback/audience-feedback-controller.js +1 -1
  321. package/core/server/services/audience-feedback/audience-feedback-service.js +2 -2
  322. package/core/server/services/automations/automations-repository.ts +3 -1
  323. package/core/server/services/automations/database-automations-repository.js +33 -14
  324. package/core/server/services/automations/database-automations-repository.ts +41 -15
  325. package/core/server/services/automations/get-scheduler-idempotency-key.js +13 -0
  326. package/core/server/services/automations/get-scheduler-idempotency-key.ts +8 -0
  327. package/core/server/services/automations/poll.js +3 -1
  328. package/core/server/services/automations/poll.ts +4 -1
  329. package/core/server/services/automations/service.js +7 -3
  330. package/core/server/services/automations/service.ts +7 -3
  331. package/core/server/services/comments/comments-service-emails.js +1 -1
  332. package/core/server/services/comments/comments-service.js +1 -1
  333. package/core/server/services/email-analytics/email-analytics-service-wrapper.js +11 -2
  334. package/core/server/services/email-analytics/email-analytics-service.js +1 -1
  335. package/core/server/services/email-service/email-service-wrapper.js +2 -1
  336. package/core/server/services/email-service/mailgun-email-provider.js +15 -0
  337. package/core/server/services/gift-links/CONTEXT.md +9 -0
  338. package/core/server/services/gifts/CONTEXT.md +55 -0
  339. package/core/server/services/gifts/README.md +28 -0
  340. package/core/server/services/gifts/gift-bookshelf-repository.js +65 -45
  341. package/core/server/services/gifts/gift-bookshelf-repository.ts +203 -74
  342. package/core/server/services/gifts/gift-checkout-adapter.js +59 -0
  343. package/core/server/services/gifts/gift-checkout-offer.js +74 -0
  344. package/core/server/services/gifts/gift-checkout-offer.ts +127 -0
  345. package/core/server/services/gifts/gift-codec.js +63 -0
  346. package/core/server/services/gifts/gift-codec.ts +62 -0
  347. package/core/server/services/gifts/gift-controller.js +6 -31
  348. package/core/server/services/gifts/gift-controller.ts +6 -69
  349. package/core/server/services/gifts/gift-email-service.ts +3 -2
  350. package/core/server/services/gifts/gift-schema.js +34 -0
  351. package/core/server/services/gifts/gift-schema.ts +39 -0
  352. package/core/server/services/gifts/gift-service-wrapper.js +13 -11
  353. package/core/server/services/gifts/gift-service.js +265 -52
  354. package/core/server/services/gifts/gift-service.ts +431 -97
  355. package/core/server/services/gifts/gift.ts +16 -39
  356. package/core/server/services/indexnow-ping/indexnow-ping-service.js +1 -1
  357. package/core/server/services/indexnow-ping/indexnow-ping-service.ts +2 -4
  358. package/core/server/services/lib/mailgun-client.js +3 -3
  359. package/core/server/services/link-tracking/link-click-repository.js +6 -1
  360. package/core/server/services/link-tracking/link-click-tracking-service.js +12 -3
  361. package/core/server/services/media-inliner/external-media-inliner.js +12 -8
  362. package/core/server/services/member-attribution/url-translator.js +5 -7
  363. package/core/server/services/member-welcome-emails/member-welcome-email-renderer.js +4 -3
  364. package/core/server/services/member-welcome-emails/service.js +79 -15
  365. package/core/server/services/members/api.js +2 -3
  366. package/core/server/services/members/import-export/import/completion-email.js +8 -4
  367. package/core/server/services/members/import-export/import/completion-email.ts +9 -4
  368. package/core/server/services/members/import-export/import/importer.js +25 -4
  369. package/core/server/services/members/import-export/import/importer.ts +27 -6
  370. package/core/server/services/members/import-export/import/row.ts +5 -1
  371. package/core/server/services/members/import-export/index.js +6 -2
  372. package/core/server/services/members/import-export/index.ts +10 -4
  373. package/core/server/services/members/members-api/controllers/router-controller.js +38 -36
  374. package/core/server/services/members/members-api/members-api.js +13 -7
  375. package/core/server/services/members/members-api/repositories/event-repository.js +4 -94
  376. package/core/server/services/members/members-api/services/member-bread-service.js +3 -3
  377. package/core/server/services/members/members-api/services/payments-service.js +6 -68
  378. package/core/server/services/members-custom-fields/queries.js +4 -13
  379. package/core/server/services/members-custom-fields/queries.ts +4 -13
  380. package/core/server/services/members-custom-fields/schema.js +19 -21
  381. package/core/server/services/members-custom-fields/schema.ts +18 -20
  382. package/core/server/services/members-custom-fields/storage.js +94 -27
  383. package/core/server/services/members-custom-fields/storage.ts +114 -39
  384. package/core/server/services/members-custom-fields/values-service.js +98 -103
  385. package/core/server/services/members-custom-fields/values-service.ts +116 -114
  386. package/core/server/services/mentions/resource-service.js +1 -1
  387. package/core/server/services/mentions/service.js +2 -2
  388. package/core/server/services/oembed/oembed-service.js +44 -3
  389. package/core/server/services/posts/posts-service.js +11 -0
  390. package/core/server/services/route-settings/dynamic-routing-service.js +10 -62
  391. package/core/server/services/route-settings/route-settings-parser.js +25 -25
  392. package/core/server/services/route-settings/route-settings-parser.ts +27 -27
  393. package/core/server/services/route-settings/validation-errors.js +68 -4
  394. package/core/server/services/route-settings/validation-errors.ts +79 -4
  395. package/core/server/services/slack-ping/slack-ping-service.js +2 -2
  396. package/core/server/services/slack-ping/slack-ping-service.ts +3 -5
  397. package/core/server/services/stats/content-stats-service.js +3 -3
  398. package/core/server/services/stats/posts-stats-service.js +29 -25
  399. package/core/server/services/stripe/services/webhook/charge-refunded-event-service.js +1 -1
  400. package/core/server/services/stripe/services/webhook/checkout-session-event-service.js +16 -4
  401. package/core/server/services/stripe/stripe-api.js +10 -0
  402. package/core/server/services/url/config.js +90 -135
  403. package/core/server/services/url/index.js +6 -49
  404. package/core/server/services/url/lazy-find-resource.js +14 -11
  405. package/core/server/services/url/lazy-find-resource.ts +14 -11
  406. package/core/server/services/url/lazy-url-service.js +174 -94
  407. package/core/server/services/url/lazy-url-service.ts +233 -97
  408. package/core/server/services/url/routable-resources.js +62 -71
  409. package/core/server/services/url/router-filter.js +12 -4
  410. package/core/server/services/url/router-filter.ts +12 -4
  411. package/core/server/services/webhooks/index.js +1 -1
  412. package/core/server/services/webhooks/serialize.js +2 -1
  413. package/core/server/web/gift-preview/controller.js +10 -25
  414. package/core/shared/config/env/config.testing-mysql.json +1 -2
  415. package/core/shared/config/env/config.testing.json +1 -2
  416. package/core/shared/events/index.js +1 -2
  417. package/core/shared/labs.js +4 -4
  418. package/package.json +10 -10
  419. package/pnpm-lock.yaml +27 -20
  420. package/pnpm-workspace.yaml +3 -2
  421. package/tsconfig.json +1 -0
  422. package/components/tryghost-kg-default-nodes-2.1.3.tgz +0 -0
  423. package/core/built/admin/assets/Combination-CJG7Flp-.js +0 -41
  424. package/core/built/admin/assets/activitypub/layout-qx-bUL1k.js +0 -1
  425. package/core/built/admin/assets/automated-emails-DRy7qY-J.js +0 -7
  426. package/core/built/admin/assets/checkbox-CWa28lgO.js +0 -1
  427. package/core/built/admin/assets/chunk.524.cffd399783a519e404a9.js +0 -25
  428. package/core/built/admin/assets/chunk.582.eebf0d0b2d202bb746a5.js +0 -27
  429. package/core/built/admin/assets/code-editor-view-CCr0rBWS.js +0 -1
  430. package/core/built/admin/assets/comments-DyUSMUdj.js +0 -1
  431. package/core/built/admin/assets/disabled-sources-indicator-CUxMfiPa.js +0 -1
  432. package/core/built/admin/assets/dist-2Tnz2Yfq.js +0 -1
  433. package/core/built/admin/assets/dist-CjY-f5Z1.js +0 -1
  434. package/core/built/admin/assets/dist-DDY6GRVz.js +0 -1
  435. package/core/built/admin/assets/dist-DG78yroX.js +0 -23
  436. package/core/built/admin/assets/domain-C0bV2UV8.js +0 -1
  437. package/core/built/admin/assets/editor-Dp865O-4.js +0 -8
  438. package/core/built/admin/assets/esm-CFX93H5M.js +0 -2
  439. package/core/built/admin/assets/filter-sources-BV2rnpfA.js +0 -1
  440. package/core/built/admin/assets/filters-CUXJ2Nm9.js +0 -1
  441. package/core/built/admin/assets/filters-DfbIWc_l.js +0 -1
  442. package/core/built/admin/assets/index-9Cw8z3rA.css +0 -1
  443. package/core/built/admin/assets/index-BGG7NckO.js +0 -67
  444. package/core/built/admin/assets/input-group-BSgG_lVw.js +0 -1
  445. package/core/built/admin/assets/label-picker-1DvSuLA1.js +0 -1
  446. package/core/built/admin/assets/labels-CDSUt4J_.js +0 -1
  447. package/core/built/admin/assets/layout-yQEi2qDM.js +0 -1
  448. package/core/built/admin/assets/link-helpers-7EYqvvHs.js +0 -1
  449. package/core/built/admin/assets/member-custom-fields-yOMHyAg9.js +0 -1
  450. package/core/built/admin/assets/member-detail-BAq7gDaa.js +0 -1
  451. package/core/built/admin/assets/members-CQpVFJu4.js +0 -10
  452. package/core/built/admin/assets/modals-ReORvWnu.js +0 -31
  453. package/core/built/admin/assets/newsletter-CJQvtHqN.js +0 -1
  454. package/core/built/admin/assets/newsletters-B3QQXfGT.js +0 -1
  455. package/core/built/admin/assets/nql-BsgFMd93.js +0 -1
  456. package/core/built/admin/assets/overview-Bf2sbznq.js +0 -1
  457. package/core/built/admin/assets/overview-CBfePxcr.js +0 -1
  458. package/core/built/admin/assets/post-analytics-header-Bmc9su6q.js +0 -1
  459. package/core/built/admin/assets/preferences-DJPfHEGu.js +0 -5
  460. package/core/built/admin/assets/profile-DqBQp-gT.js +0 -1
  461. package/core/built/admin/assets/select-C5_xEt5b.js +0 -1
  462. package/core/built/admin/assets/settings-BwiVIXau.js +0 -4
  463. package/core/built/admin/assets/social-urls-BQPFEk1_.js +0 -8
  464. package/core/built/admin/assets/switch-z5Y6Uzt6.js +0 -1
  465. package/core/built/admin/assets/tabs-CCEgKYzq.js +0 -1
  466. package/core/built/admin/assets/tags-Cg2VJGmG.js +0 -1
  467. package/core/built/admin/assets/textarea-Bjccvh_E.js +0 -1
  468. package/core/built/admin/assets/toggle-group-b3IlxvhV.js +0 -1
  469. package/core/built/admin/assets/web-B_5GZRvH.js +0 -1
  470. package/core/server/services/gifts/gift-repository.js +0 -2
  471. package/core/server/services/gifts/gift-repository.ts +0 -29
  472. package/core/server/services/route-settings/activation-bridge.js +0 -174
  473. package/core/server/services/route-settings/activation-bridge.ts +0 -250
  474. package/core/server/services/url/local-file-cache.js +0 -75
  475. package/core/server/services/url/queue.js +0 -260
  476. package/core/server/services/url/resource.js +0 -102
  477. package/core/server/services/url/resources.js +0 -480
  478. package/core/server/services/url/url-generator.js +0 -276
  479. package/core/server/services/url/url-service-facade.js +0 -366
  480. package/core/server/services/url/url-service-facade.ts +0 -519
  481. package/core/server/services/url/url-service.js +0 -387
  482. package/core/server/services/url/urls.js +0 -140
  483. package/core/shared/events/url-resource-updated-event.js +0 -33
@@ -3,28 +3,35 @@ 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 {DbCustomFieldLeaf, DbCustomFieldValue, FIELD_STATUS} from './schema';
8
8
  import {activeFields} from './queries';
9
- import {storageCodecFor, storageColumnsFor} from './storage';
9
+ import {leavesToWrite, valuesFromLeaves, type StoredLeaf} from './storage';
10
10
 
11
11
  const FIELDS_TABLE = 'members_custom_fields';
12
12
  const VALUES_TABLE = 'members_custom_field_values';
13
13
 
14
- // Matches the `members_custom_fields.key` column (schema.js), so no key a site
15
- // could actually have minted is ever refused by it.
14
+ /** Matches the `members_custom_fields.key` column, so no key a site could hold is refused. */
16
15
  const MAX_KEY_LENGTH = 191;
17
16
 
18
- // Values arrive keyed by field key. Each value stays `unknown` here: it is
19
- // validated by its own field type's schema, which isn't known until the key is
20
- // resolved to a definition.
17
+ /**
18
+ * Rows per insert statement, bounded by knex rather than by either database. SQLite takes
19
+ * a multi-row `VALUES` perfectly well; knex's SQLite dialect emits that form only for a
20
+ * single row and compiles anything longer into `INSERT ... SELECT ? UNION ALL SELECT ?`,
21
+ * which SQLite refuses past 500 terms.
22
+ *
23
+ * Open as knex#721 since 2015, with an approved but unmerged fix in knex#5780. This can
24
+ * go when that lands; until then the alternative is hand-written upsert SQL per engine.
25
+ */
26
+ const UPSERT_CHUNK = 400;
27
+
28
+ /** Derived, not restated, so a column changing shape in `schema.ts` changes here too. */
29
+ type DbLeafRow = z.infer<typeof DbCustomFieldValue>;
30
+
31
+ // Values stay `unknown`: each is validated by its own field type, which is not known
32
+ // until the key is resolved to a definition.
21
33
  const ValuesInput = z.record(z.string().max(MAX_KEY_LENGTH), z.unknown());
22
34
 
23
- // The field facts the value path needs: the id to write the FK, the key to match
24
- // input against, the name for error messages, and the type to pick the validator
25
- // and storage column. All columns on members_custom_fields, so a value operation
26
- // resolves them with a direct query rather than routing through the definitions
27
- // service — the same way the read path already joins the table directly.
28
35
  interface ActiveField {
29
36
  id: string;
30
37
  key: string;
@@ -32,30 +39,20 @@ interface ActiveField {
32
39
  type: FieldType;
33
40
  }
34
41
 
35
- // A resolved, validated write. `value` absent means clear (delete the row).
42
+ /** An absent `value` means clear the field. */
36
43
  interface PlannedWrite {
37
44
  field: ActiveField;
38
45
  value?: unknown;
39
46
  }
40
47
 
41
48
  /**
42
- * The value half of member custom fields: reading and writing what a member holds
43
- * for each defined field. It belongs to the member aggregate a value is data
44
- * about a member while field definitions belong to the site's settings; that
45
- * aggregate boundary, not a technical layer, is why this is a separate service.
46
- *
47
- * It owns the `members_custom_field_values` table and reads `members_custom_fields`
48
- * directly for the reference data it needs (which fields are active, and how they
49
- * validate), rather than calling the definitions service — the same way the other
50
- * knex services here talk to the database.
49
+ * What a member holds for each defined field. Separate from the definitions service
50
+ * because a value belongs to the member and a definition belongs to the site's settings,
51
+ * which are different aggregates rather than different layers.
51
52
  */
52
53
  export class CustomFieldValuesService {
53
54
  private knex: Knex;
54
- /**
55
- * @private
56
- * A getter rather than a number: the ceiling is an operator setting that can
57
- * change between requests.
58
- */
55
+ /** A getter, not a number: the ceiling is an operator setting that changes between requests. */
59
56
  private getMaxDefinitions: () => number;
60
57
 
61
58
  constructor({knex, getMaxDefinitions}: {knex: Knex, getMaxDefinitions: () => number}) {
@@ -63,11 +60,6 @@ export class CustomFieldValuesService {
63
60
  this.getMaxDefinitions = getMaxDefinitions;
64
61
  }
65
62
 
66
- /**
67
- * The active fields for a set of keys, keyed by key, for resolving a write.
68
- * Scoped to the keys the caller is actually writing rather than every active
69
- * field, since a publisher may have defined many and an edit touches few.
70
- */
71
63
  private async activeFieldsByKey(keys: string[]): Promise<Map<string, ActiveField>> {
72
64
  if (keys.length === 0) {
73
65
  return new Map();
@@ -79,63 +71,39 @@ export class CustomFieldValuesService {
79
71
  }
80
72
 
81
73
  /**
82
- * Members' values for every active field, keyed by member id, then by field
83
- * key. A member with no values is absent from the outer map; a field a member
84
- * has no value for is absent from the inner one — "not set" is the absence of
85
- * a key, told apart from a value by not being there. Archived fields are
86
- * excluded to match the definitions `browse`: their values stay in the
87
- * database, they just stop being addressable.
88
- *
89
- * One query for the whole set, so a member list can't turn into an N+1.
74
+ * Members' values, keyed by member id then field key; anything absent is unset.
90
75
  *
91
- * Reading is best-effort per value: a stored value whose type has since left
92
- * the catalog, or that won't decode, is omitted and logged rather than failing
93
- * the whole read. Stored data outlives the catalog, so one stale row must not
94
- * take down a member's payload.
76
+ * Archived fields are excluded to match the definitions browse: their values stay in
77
+ * the database and stop being addressable. A row that will not parse is dropped and
78
+ * logged rather than failing the read, so one stale row cannot take down a member.
95
79
  */
96
80
  async getValuesForMembers(memberIds: string[]): Promise<Map<string, Record<string, unknown>>> {
97
- const byMember = new Map<string, Record<string, unknown>>();
98
81
  if (memberIds.length === 0) {
99
- return byMember;
82
+ return new Map();
100
83
  }
101
84
 
102
85
  const rows = await this.knex(VALUES_TABLE)
103
86
  .join(FIELDS_TABLE, `${VALUES_TABLE}.custom_field_id`, `${FIELDS_TABLE}.id`)
104
87
  .whereIn(`${VALUES_TABLE}.member_id`, memberIds)
105
- // Read join, so the status filter is qualified and lives inline — it
106
- // shares the invariant's vocabulary with queries.ts, not its builder.
107
88
  .where(`${FIELDS_TABLE}.status`, FIELD_STATUS.active)
108
89
  .orderBy(`${FIELDS_TABLE}.created_at`, 'asc')
109
90
  .orderBy(`${FIELDS_TABLE}.id`, 'asc')
110
- .select(`${VALUES_TABLE}.member_id`, `${FIELDS_TABLE}.key`, `${FIELDS_TABLE}.type`, `${VALUES_TABLE}.value_text`, `${VALUES_TABLE}.value_json`);
91
+ .orderBy(`${VALUES_TABLE}.path`, 'asc')
92
+ .select(`${VALUES_TABLE}.member_id`, `${FIELDS_TABLE}.key`, `${FIELDS_TABLE}.type`, `${VALUES_TABLE}.path`, `${VALUES_TABLE}.value_text`);
111
93
 
94
+ const leaves: StoredLeaf[] = [];
112
95
  for (const row of rows) {
113
96
  try {
114
- const {member_id: memberId, key, type, ...stored} = DbCustomFieldValueWithField.parse(row);
115
- const codec = storageCodecFor(type);
116
- const raw = stored[codec.column];
117
- // The column the type routes to is the only one that can hold this
118
- // field's value; a null there means the row is not carrying one.
119
- if (raw === null) {
120
- continue;
121
- }
122
- const values = byMember.get(memberId) ?? {};
123
- values[key] = codec.decode(raw);
124
- byMember.set(memberId, values);
97
+ leaves.push(DbCustomFieldLeaf.parse(row));
125
98
  } catch (err) {
126
- logging.warn(`Skipping undecodable custom field value (field '${row.key}', type '${row.type}'): ${err instanceof Error ? err.message : String(err)}`);
99
+ logging.warn(`Skipping unreadable custom field value (field '${row.key}', path '${row.path}'): ${err instanceof Error ? err.message : String(err)}`);
127
100
  }
128
101
  }
129
102
 
130
- return byMember;
103
+ return valuesFromLeaves(leaves);
131
104
  }
132
105
 
133
- /**
134
- * @private
135
- * Input as the values object it claims to be, rejecting anything that isn't
136
- * one. Shared by every caller so they cannot disagree on what a values object
137
- * is, or on the error when it isn't one.
138
- */
106
+ /** Shared by every caller so they cannot disagree on what a values object is. */
139
107
  private parseValues(input: unknown): Record<string, unknown> {
140
108
  const parsed = ValuesInput.safeParse(input);
141
109
  if (!parsed.success) {
@@ -146,12 +114,8 @@ export class CustomFieldValuesService {
146
114
  }
147
115
 
148
116
  /**
149
- * Whether input names any values. An absent key names none; anything present
150
- * that isn't a values object throws, with the same error resolving it would
151
- * have raised.
152
- *
153
- * Answers the shape question alone, with no catalog lookup, so it can be asked
154
- * before a write is known to be permitted.
117
+ * Whether input names any values. Asks the shape question alone, with no catalog
118
+ * lookup, so it can be asked before a write is known to be permitted.
155
119
  */
156
120
  namesValues(input: unknown): boolean {
157
121
  if (input === undefined) {
@@ -162,20 +126,16 @@ export class CustomFieldValuesService {
162
126
  }
163
127
 
164
128
  /**
165
- * Resolve input into the writes it implies, rejecting anything invalid, and
166
- * writing nothing. Returned so a caller can validate before it commits to a
167
- * change it would have to unwind (the member edit and the importer both validate
168
- * up front, before opening a transaction), then apply the same plan without
169
- * re-resolving or re-validating.
129
+ * Resolve input into the writes it implies, writing nothing. Returned so a caller can
130
+ * validate before opening a transaction it would otherwise have to unwind, then apply
131
+ * the same plan without re-resolving it.
170
132
  */
171
133
  async planWrite(input: unknown): Promise<PlannedWrite[]> {
172
134
  const values = this.parseValues(input);
173
135
  const keys = Object.keys(values);
174
136
 
175
- // A write cannot name more fields than the site may define, so the
176
- // definitions ceiling bounds it. This also holds the lookup below within the
177
- // database driver's bound-parameter limit, which one key per parameter would
178
- // otherwise exceed.
137
+ // Bounded by the definitions ceiling, which also holds the lookup below inside
138
+ // the driver's bound-parameter limit.
179
139
  const maxKeys = this.getMaxDefinitions();
180
140
  if (keys.length > maxKeys) {
181
141
  throw new errors.ValidationError({
@@ -190,29 +150,29 @@ export class CustomFieldValuesService {
190
150
  for (const [key, raw] of Object.entries(values)) {
191
151
  const field = byKey.get(key);
192
152
  if (!field) {
193
- // Unknown (or archived) key. Rejected rather than ignored: a typo
194
- // that silently drops a value is worse than a failed save.
153
+ // Unknown or archived. Refused rather than ignored: a typo that silently
154
+ // drops what somebody typed is worse than a save that fails. The catalog
155
+ // applies the same rule to the parts of a composite value.
195
156
  throw new errors.ValidationError({message: `Unknown custom field: ${key}`, property: `custom_fields.${key}`});
196
157
  }
197
158
 
198
- // `null` clears any field. An empty string clears a text-backed field
199
- // an emptied input but for any other storage type '' is a value to
200
- // validate, not a clear, so nothing is coerced: a string sent to a
201
- // (future) non-text field fails validation rather than silently deleting.
202
- if (raw === null || (raw === '' && FIELD_TYPES[field.type].storageType === 'text')) {
159
+ // `null` clears any field, and `''` clears one with no parts. For a value
160
+ // with parts `''` names nothing, so it is left to fail validation rather than
161
+ // being read as a silent delete.
162
+ if (raw === null || (raw === '' && subFieldsOf(field.type) === null)) {
203
163
  writes.push({field});
204
164
  continue;
205
165
  }
206
166
 
207
- // The field type owns what a valid value is core just runs it. For a
208
- // composite the issue path points at the offending sub-field, so the
209
- // property reads `custom_fields.home_address.postal_code`.
167
+ // Message only, no `context`: the API error handler moves a message into
168
+ // `context` when `context` is empty and prepends it when it is not, so
169
+ // anything added here reaches the client glued to the front of the reason.
170
+ // Which field failed rides in `property`.
210
171
  const value = FIELD_TYPES[field.type].value.safeParse(raw);
211
172
  if (!value.success) {
212
173
  const issue = value.error.issues[0];
213
174
  throw new errors.ValidationError({
214
- message: `Invalid value for custom field '${field.name}'.`,
215
- context: issue.message,
175
+ message: issue.message,
216
176
  property: [`custom_fields.${key}`, ...issue.path].join('.')
217
177
  });
218
178
  }
@@ -225,39 +185,81 @@ export class CustomFieldValuesService {
225
185
  /**
226
186
  * Apply a plan from `planWrite`.
227
187
  *
228
- * Merge, not replace: only the fields in the plan are touched, so a caller
229
- * that doesn't know about a field can't erase it.
188
+ * A write touches the paths it names and nothing else, at every level: naming a path
189
+ * with an empty value clears that part, naming the field with `null` clears all of
190
+ * them, and saying nothing about a path leaves it alone. There is no whole-value
191
+ * replace, so a caller that does not know about a field cannot erase it.
230
192
  *
231
- * Always transactional, so a mid-batch failure rolls the batch back. Passed an
232
- * executor it joins that transaction -- the importer passes its per-member one, so a
233
- * failed value write takes the member with it; passed nothing it opens its own.
193
+ * Always transactional. Given an executor it joins that transaction, so the importer's
194
+ * failed value write takes its member with it; given none it opens its own.
234
195
  */
235
- async applyWrite(memberId: string, writes: PlannedWrite[], executor: Knex = this.knex): Promise<void> {
196
+ async applyWrite(
197
+ memberId: string,
198
+ writes: PlannedWrite[],
199
+ {executor = this.knex}: {executor?: Knex} = {}
200
+ ): Promise<void> {
236
201
  if (writes.length === 0) {
237
202
  return;
238
203
  }
239
204
 
240
205
  const apply = async (trx: Knex) => {
241
- for (const {field, value} of writes) {
242
- const target = {member_id: memberId, custom_field_id: field.id};
206
+ // Built first, then sent as whole statements: a handful per member rather
207
+ // than one per part, under one timestamp, because a write happened once
208
+ // however many rows record it.
209
+ const now = new Date();
210
+ const clearedFields: string[] = [];
211
+ const clearedPaths: Array<{fieldId: string, paths: string[]}> = [];
212
+ const rows: DbLeafRow[] = [];
243
213
 
214
+ for (const {field, value} of writes) {
244
215
  if (value === undefined) {
245
- await trx(VALUES_TABLE).where(target).del();
216
+ clearedFields.push(field.id);
246
217
  continue;
247
218
  }
248
219
 
249
- const valueColumns = storageColumnsFor(field.type, value);
250
- await trx(VALUES_TABLE)
251
- .insert({id: new ObjectID().toHexString(), ...target, ...valueColumns, created_at: new Date()})
252
- // The unique index on (member_id, custom_field_id) is
253
- // what makes this an upsert rather than a read-then-write race.
254
- .onConflict(['member_id', 'custom_field_id'])
255
- .merge({...valueColumns, updated_at: new Date()});
220
+ const {set, cleared} = leavesToWrite(value);
221
+ if (cleared.length > 0) {
222
+ clearedPaths.push({fieldId: field.id, paths: cleared});
223
+ }
224
+
225
+ rows.push(...set.map(leaf => ({
226
+ id: new ObjectID().toHexString(),
227
+ member_id: memberId,
228
+ custom_field_id: field.id,
229
+ path: leaf.path,
230
+ value_text: leaf.value_text,
231
+ created_at: now,
232
+ updated_at: now
233
+ })));
234
+ }
235
+
236
+ if (clearedFields.length > 0) {
237
+ await trx(VALUES_TABLE).where('member_id', memberId).whereIn('custom_field_id', clearedFields).del();
238
+ }
239
+
240
+ if (clearedPaths.length > 0) {
241
+ // One statement with a group per field, rather than a statement per field.
242
+ await trx(VALUES_TABLE).where('member_id', memberId).where((builder) => {
243
+ for (const {fieldId, paths} of clearedPaths) {
244
+ builder.orWhere(pair => pair.where('custom_field_id', fieldId).whereIn('path', paths));
245
+ }
246
+ }).del();
247
+ }
248
+
249
+ for (let from = 0; from < rows.length; from += UPSERT_CHUNK) {
250
+ // Typed as the plain row because `merge` takes its columns as `keyof` the
251
+ // builder's record, which for a composite table registration is the scope
252
+ // names rather than the columns.
253
+ await trx<DbLeafRow>(VALUES_TABLE)
254
+ .insert(rows.slice(from, from + UPSERT_CHUNK))
255
+ // Naming the columns rather than giving values takes each from the row
256
+ // that lost the conflict, so every part updates to its own value.
257
+ .onConflict(['member_id', 'custom_field_id', 'path'])
258
+ .merge(['value_text', 'updated_at']);
256
259
  }
257
260
  };
258
261
 
259
- // isTransaction is knex's marker for a transactor: join an existing transaction
260
- // rather than nesting a savepoint under it, otherwise open one.
262
+ // knex's marker for a transactor: join it rather than nesting a savepoint under it.
261
263
  if (executor.isTransaction) {
262
264
  await apply(executor);
263
265
  } else {
@@ -30,7 +30,7 @@ module.exports = class ResourceService {
30
30
  */
31
31
  async getByURL(url) {
32
32
  const path = this.#urlUtils.absoluteToRelative(url.href, {withoutSubdirectory: true});
33
- const resource = await this.#urlService.facade.resolveUrl(path);
33
+ const resource = await this.#urlService.resolveUrl(path);
34
34
  if (resource?.type === 'posts') {
35
35
  return {
36
36
  type: 'post',
@@ -20,7 +20,7 @@ const jobsService = require('../mentions-jobs');
20
20
  // relations it reads for filtered collections (event-emitted models don't
21
21
  // reliably carry them).
22
22
  async function getPostData(post) {
23
- const missing = urlService.facade.getRequiredRelations().filter(relation => !post.relations[relation]);
23
+ const missing = urlService.getRequiredRelations().filter(relation => !post.relations[relation]);
24
24
  if (missing.length) {
25
25
  await post.load(missing);
26
26
  }
@@ -31,7 +31,7 @@ function getPostUrl(id, postData) {
31
31
  const jsonModel = {...postData};
32
32
  // The URL service routes by resource type. Pages and posts share the Post
33
33
  // model, so the page's own type must reach forPost — otherwise it defaults
34
- // to 'posts', matches no post collection, and 404s under the lazy service.
34
+ // to 'posts', matches no post collection, and 404s.
35
35
  const type = postData.type === 'page' ? 'pages' : 'posts';
36
36
  outputSerializerUrlUtil.forPost(id, jsonModel, {options: {}}, type);
37
37
  return jsonModel.url;
@@ -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});
@@ -1,11 +1,6 @@
1
1
  const debug = require('@tryghost/debug')('services:route-settings:service');
2
2
  const logging = require('@tryghost/logging');
3
3
  const errors = require('@tryghost/errors');
4
- const tpl = require('@tryghost/tpl');
5
-
6
- const messages = {
7
- loadError: 'Could not load routes.yaml file.'
8
- };
9
4
 
10
5
  function isStoredContentError(err) {
11
6
  return err.errorType === 'ValidationError' || err.errorType === 'IncorrectUsageError';
@@ -25,8 +20,7 @@ class DynamicRoutingService {
25
20
 
26
21
  /**
27
22
  * Wire the storage-layer dependency so the API surface (upload, download)
28
- * works immediately after boot even when the frontend is disabled and
29
- * `start()` is never called.
23
+ * works from early boot, well before `start()` wires up routing.
30
24
  *
31
25
  * @param {object} deps
32
26
  * @param {RouteSettingsStore} deps.store - adapter-manager provided store
@@ -38,11 +32,13 @@ class DynamicRoutingService {
38
32
 
39
33
  /**
40
34
  * Wire the routing dependencies and load route settings into the router.
41
- * Called from initDynamicRouting in boot.js only when the frontend is enabled.
35
+ * Called from initDynamicRouting in boot.js on every boot: the URL service
36
+ * has to learn the routes even where no page is served, so that the APIs and
37
+ * background services can build URLs.
42
38
  *
43
39
  * @param {object} deps
44
40
  * @param {object} deps.routerManager - frontend RouterManager singleton
45
- * @param {object} deps.urlService - UrlServiceFacade
41
+ * @param {object} deps.urlService - the URL service singleton
46
42
  */
47
43
  async start({routerManager, urlService}) {
48
44
  debug('start');
@@ -54,10 +50,8 @@ class DynamicRoutingService {
54
50
  }
55
51
 
56
52
  async loadRouteSettings() {
57
- const {buildRouterSettings} = require('./activation-bridge');
58
-
59
53
  try {
60
- return buildRouterSettings(await this.store.get());
54
+ return await this.store.get();
61
55
  } catch (err) {
62
56
  // A stored-content error means the site's routes.yaml is invalid —
63
57
  // either it fails validation or it isn't parseable YAML. Log a
@@ -92,60 +86,14 @@ class DynamicRoutingService {
92
86
  // Parse and validate before anything is persisted — an invalid
93
87
  // upload is rejected here and never reaches the store.
94
88
  const next = parseRouteSettings(parseYaml(yamlContent), yamlContent);
95
- let previous = null;
96
- try {
97
- previous = await this.store.get();
98
- } catch (err) {
99
- if (!isStoredContentError(err)) {
100
- throw err;
101
- }
102
- }
103
89
 
104
90
  await this.store.replace(next);
105
91
 
106
- urlService.resetGenerators({releaseResourcesOnly: true});
107
-
108
- const bringBackValidRoutes = async () => {
109
- urlService.resetGenerators({releaseResourcesOnly: true});
110
-
111
- if (previous) {
112
- await this.store.replace(previous);
113
- }
114
-
115
- return bridge.reloadFrontend(this, urlService);
116
- };
117
-
92
+ // Registration is synchronous inside the reload: RouterManager.start()
93
+ // always mounts the static pages router, which has permalinks, so the
94
+ // URL service has routers again — and reports ready — by the time this
95
+ // resolves. Nothing left to wait for.
118
96
  await bridge.reloadFrontend(this, urlService);
119
-
120
- let tries = 0;
121
-
122
- function isBlogRunning() {
123
- debug('waiting for blog running');
124
- return new Promise((resolve) => {
125
- setTimeout(resolve, 1000);
126
- })
127
- .then(() => {
128
- debug('waited for blog running');
129
- if (!urlService.hasFinished()) {
130
- if (tries > 5) {
131
- throw new errors.InternalServerError({
132
- message: tpl(messages.loadError)
133
- });
134
- }
135
-
136
- tries = tries + 1;
137
- return isBlogRunning();
138
- }
139
- });
140
- }
141
-
142
- return isBlogRunning()
143
- .catch((err) => {
144
- return bringBackValidRoutes()
145
- .finally(() => {
146
- throw err;
147
- });
148
- });
149
97
  }
150
98
  }
151
99