ghost 6.53.0 → 6.54.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 (596) hide show
  1. package/components/ghost-storage-base-3.0.0.tgz +0 -0
  2. package/components/tryghost-adapter-base-cache-0.2.0.tgz +0 -0
  3. package/components/tryghost-adapter-base-scheduling-0.1.1.tgz +0 -0
  4. package/components/tryghost-adapter-base-sso-0.1.1.tgz +0 -0
  5. package/components/tryghost-admin-api-schema-0.0.0.tgz +0 -0
  6. package/components/tryghost-custom-field-types-0.0.0.tgz +0 -0
  7. package/components/tryghost-i18n-0.0.0.tgz +0 -0
  8. package/components/tryghost-kg-converters-1.2.3.tgz +0 -0
  9. package/components/tryghost-kg-default-nodes-2.1.3.tgz +0 -0
  10. package/components/tryghost-kg-lexical-html-renderer-1.4.3.tgz +0 -0
  11. package/core/boot.js +9 -6
  12. package/core/built/admin/assets/Combination-CJG7Flp-.js +41 -0
  13. package/core/built/admin/assets/Growth-CSaHoSVD.js +3 -0
  14. package/core/built/admin/assets/Newsletters-C5cs6FGo.js +1 -0
  15. package/core/built/admin/assets/Overview-j4NaeUNm.js +1 -0
  16. package/core/built/admin/assets/{PolarAngleAxis-pciQ0MOy.js → PolarAngleAxis-BGjf7sBu.js} +1 -1
  17. package/core/built/admin/assets/{RadialBarChart-DWmw2PcX.js → RadialBarChart-zeFf9mBz.js} +1 -1
  18. package/core/built/admin/assets/Web-_fD935OX.js +2 -0
  19. package/core/built/admin/assets/account-migration-DLvVA5yq.js +1 -0
  20. package/core/built/admin/assets/activitypub/{account-migration-BPZG37DJ.js → account-migration-DHbZcxGM.js} +13 -13
  21. package/core/built/admin/assets/activitypub/activitypub.js +7 -7
  22. package/core/built/admin/assets/activitypub/{avatar-ZYco0ITb.js → avatar-B3DEDB1l.js} +10 -10
  23. package/core/built/admin/assets/activitypub/{avatar-flipboard-D541lc4F.js → avatar-flipboard-Cqrg0XMx.js} +3 -3
  24. package/core/built/admin/assets/activitypub/{bluesky-sharing-NOHUtHjl.js → bluesky-sharing-zfxUey_L.js} +38 -37
  25. package/core/built/admin/assets/activitypub/{chunk-KS7C4IRE-B-GOFDPZ.js → chunk-KS7C4IRE-D4HEcEwi.js} +716 -710
  26. package/core/built/admin/assets/activitypub/{content-formatters-bwrp4iEO.js → content-formatters-BpQARh28.js} +229 -181
  27. package/core/built/admin/assets/activitypub/{copy-C7m7ovHw.js → copy-D9UWIAan.js} +2 -2
  28. package/core/built/admin/assets/activitypub/{dist-B5ira32t.js → dist-Ba_QxHHl.js} +5 -5
  29. package/core/built/admin/assets/activitypub/{domain-iqE4YXwD.js → domain-CmwSZLHK.js} +33 -33
  30. package/core/built/admin/assets/activitypub/{edit-profile-B8MFYh28.js → edit-profile-CXdwNmVG.js} +46 -45
  31. package/core/built/admin/assets/activitypub/{explore-BbdKodp3.js → explore-DfbjzGM1.js} +20 -20
  32. package/core/built/admin/assets/activitypub/{feed-Cl3A5owG.js → feed-DSEBJOqf.js} +15 -15
  33. package/core/built/admin/assets/activitypub/{field-DL6WyR-c.js → field-CY7XVcx6.js} +13 -13
  34. package/core/built/admin/assets/activitypub/{hash-6G3DzwL9.js → hash-BC9Q0N4c.js} +2 -2
  35. package/core/built/admin/assets/activitypub/{inbox-YmhuBice.js → inbox-DQ4ZnC1m.js} +2 -2
  36. package/core/built/admin/assets/activitypub/inbox-DtQ0kytT.js +1221 -0
  37. package/core/built/admin/assets/activitypub/{label-DGbLjDvE.js → label-Dw96GQ7u.js} +6 -6
  38. package/core/built/admin/assets/activitypub/layout-IdVq3lRy.js +1 -0
  39. package/core/built/admin/assets/activitypub/{moderation-qnBtk1zS.js → moderation-Dg_-I-8C.js} +32 -31
  40. package/core/built/admin/assets/activitypub/{note-C7ZRoSJv.js → note-Cy1-uKMY.js} +21 -21
  41. package/core/built/admin/assets/activitypub/{notifications-DiEA4b1o.js → notifications-B_fvoLIM.js} +17 -17
  42. package/core/built/admin/assets/activitypub/{onboarding-_JVgEwgV.js → onboarding-BFDkzeR2.js} +2 -2
  43. package/core/built/admin/assets/activitypub/{onboarding-DG4zu3qM.js → onboarding-D6rcBdPe.js} +5 -5
  44. package/core/built/admin/assets/activitypub/{preferences-B3FtocnP.js → preferences-ByJseZp6.js} +1607 -1606
  45. package/core/built/admin/assets/activitypub/{profile-D8ZJfYXB.js → profile-BFtbnIjz.js} +264 -263
  46. package/core/built/admin/assets/activitypub/{reply-C1vggenw.js → reply-CpdMeAO5.js} +2 -2
  47. package/core/built/admin/assets/activitypub/{routes-BOii-V9n.js → routes-Dh3xXfce.js} +1311 -1295
  48. package/core/built/admin/assets/activitypub/{separator-1gNokN5S.js → separator-BI1FLsYM.js} +4 -4
  49. package/core/built/admin/assets/activitypub/{settings--9Sb0nbl.js → settings-DTgqJPxM.js} +14 -14
  50. package/core/built/admin/assets/activitypub/{site-fkMmqv0V.js → site-CdL9ZLqm.js} +10 -10
  51. package/core/built/admin/assets/activitypub/{step-1-QhivSMw2.js → step-1-DIDg4aHb.js} +15 -15
  52. package/core/built/admin/assets/activitypub/{step-2-pMk9YwqD.js → step-2-BfGe8BYt.js} +7 -7
  53. package/core/built/admin/assets/activitypub/{step-3-DvmVC1fo.js → step-3-mE9spAqw.js} +12 -12
  54. package/core/built/admin/assets/activitypub/{tabs-Dc0FEEYD.js → tabs-ogqwoBlb.js} +241 -237
  55. package/core/built/admin/assets/activitypub/{topic-filter-CuDyM5bD.js → topic-filter-DLHBZT93.js} +4 -4
  56. package/core/built/admin/assets/activitypub/use-activity-pub-queries-VzbN99u_.js +1957 -0
  57. package/core/built/admin/assets/activitypub/{use-navigate-with-base-path-Drx1kdAK.js → use-navigate-with-base-path-CAwXTmqH.js} +165 -175
  58. package/core/built/admin/assets/activitypub/{use-reply-chain-data-BHkCHcW5.js → use-reply-chain-data-CxonteFD.js} +8 -8
  59. package/core/built/admin/assets/activitypub/users-BLeiOTHc.js +1464 -0
  60. package/core/built/admin/assets/activitypub/{x-BHCEe-c2.js → x-C8tAnXly.js} +2 -2
  61. package/core/built/admin/assets/{audience-adiFehTD.js → audience-BKRdsFmy.js} +1 -1
  62. package/core/built/admin/assets/{automated-emails-UWM02CMr.js → automated-emails-Cn87L5Md.js} +1 -1
  63. package/core/built/admin/assets/automations-B7-y3dXt.js +1 -0
  64. package/core/built/admin/assets/automations-BP_aHamJ.js +1 -0
  65. package/core/built/admin/assets/{avatar-flipboard-Lz3F9fNT.js → avatar-flipboard-C6Kj3Awu.js} +1 -1
  66. package/core/built/admin/assets/bluesky-sharing-Bh36XPh4.js +1 -0
  67. package/core/built/admin/assets/box-BWbhXh21.js +1 -0
  68. package/core/built/admin/assets/breadcrumb-D34YWnaw.js +1 -0
  69. package/core/built/admin/assets/card-DGoDJzrn.js +1 -0
  70. package/core/built/admin/assets/{chart-ChpW-NzQ.js → chart-BuRT2Y6p.js} +14 -14
  71. package/core/built/admin/assets/chart-helpers-DbklaJw9.js +1 -0
  72. package/core/built/admin/assets/checkbox-DA4-1UiI.js +1 -0
  73. package/core/built/admin/assets/chunk.524.45e0875f41b71e46e849.js +25 -0
  74. package/core/built/admin/assets/chunk.582.51defd4b009f0de8d4d5.js +27 -0
  75. package/core/built/admin/assets/{chunk.831.0703a12e49f51cec3f3a.js → chunk.589.096f4d0bb775e5229aa9.js} +78 -85
  76. package/core/built/admin/assets/{chunk.432.c2caa0b20f03e71d1a35.js → chunk.962.b73442b07be930bb5d85.js} +2216 -3188
  77. package/core/built/admin/assets/{chunk.432.c2caa0b20f03e71d1a35.js.LICENSE.txt → chunk.962.b73442b07be930bb5d85.js.LICENSE.txt} +0 -63
  78. package/core/built/admin/assets/code-editor-view-BKBrkffZ.js +1 -0
  79. package/core/built/admin/assets/comments-CzVNIT8b.js +1 -0
  80. package/core/built/admin/assets/container-BYz2Ed39.js +1 -0
  81. package/core/built/admin/assets/data-list-16rkJYYi.js +1 -0
  82. package/core/built/admin/assets/date-range-select-B5vSsrpx.js +1 -0
  83. package/core/built/admin/assets/disabled-sources-indicator-NkOIoMue.js +1 -0
  84. package/core/built/admin/assets/{dist-CsekE4FL.js → dist-CQJY6SVY.js} +1 -1
  85. package/core/built/admin/assets/dist-DGzPJ_bO.js +4 -0
  86. package/core/built/admin/assets/{dist-BbGrW05X.js → dist-DyViJwIy.js} +1 -1
  87. package/core/built/admin/assets/dist-Ndy-PuGN.js +1 -0
  88. package/core/built/admin/assets/dist-P_ohUlTi.js +1 -0
  89. package/core/built/admin/assets/domain-B7_n5nfx.js +1 -0
  90. package/core/built/admin/assets/dropzone-DcuJEEUf.js +5 -0
  91. package/core/built/admin/assets/edit-profile-rvDGrS8J.js +1 -0
  92. package/core/built/admin/assets/editor-BCiIyFjm.js +8 -0
  93. package/core/built/admin/assets/empty-indicator-oX1u1Pkn.js +1 -0
  94. package/core/built/admin/assets/esm-CFX93H5M.js +2 -0
  95. package/core/built/admin/assets/explore-BZAZOg5X.js +1 -0
  96. package/core/built/admin/assets/feed-CFV8xDw9.js +1 -0
  97. package/core/built/admin/assets/field-BW-M2Zwp.js +1 -0
  98. package/core/built/admin/assets/{filter-sources-v39ihhhV.js → filter-sources-Fx8wiYYw.js} +1 -1
  99. package/core/built/admin/assets/filters-CGX_pf2N.js +1 -0
  100. package/core/built/admin/assets/filters-DtcXMZDU.js +1 -0
  101. package/core/built/admin/assets/framework-provider-CmtnPXIT.js +4 -0
  102. package/core/built/admin/assets/{ghost-b57f077997b739a8ae9a70772dc016cb.js → ghost-b1dce91ad3232cc259ee5fbbf9526243.js} +294 -290
  103. package/core/built/admin/assets/ghost-dark-c8d0ab7f515b2010c173e23e9b32ea8a.css +1 -0
  104. package/core/built/admin/assets/gift-link-modal-Bk8qpnMw.js +1 -0
  105. package/core/built/admin/assets/{gift-links-CY3GwiVi.js → gift-links-nBxJjZiQ.js} +1 -1
  106. package/core/built/admin/assets/growth-Ck2s3JjR.js +1 -0
  107. package/core/built/admin/assets/hooks-BApwwBE0.js +2 -0
  108. package/core/built/admin/assets/{inbox-TEISAmth.js → inbox-BRp1_aaC.js} +35 -35
  109. package/core/built/admin/assets/index-BBf502Wg.js +69 -0
  110. package/core/built/admin/assets/index-BCCZKxP2.css +1 -0
  111. package/core/built/admin/assets/inline-EMAm3okk.js +1 -0
  112. package/core/built/admin/assets/input-group-DjuYnNCd.js +1 -0
  113. package/core/built/admin/assets/{react-dom-C6aoWSfl.js → jsx-runtime-DX3WBWVO.js} +1 -1
  114. package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +5 -2
  115. package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +2 -2
  116. package/core/built/admin/assets/{koenig-lexical-Di7b48ED.js → koenig-lexical-CM7UFNdL.js} +2 -2
  117. package/core/built/admin/assets/kpi-card-BrGH6FsT.js +1 -0
  118. package/core/built/admin/assets/kpi-card-CIGyijjx.js +1 -0
  119. package/core/built/admin/assets/kpi-helpers-_HRRsklE.js +1 -0
  120. package/core/built/admin/assets/kpi-tabs-BSvh9eoq.js +1 -0
  121. package/core/built/admin/assets/label-BOR9XdKl.js +1 -0
  122. package/core/built/admin/assets/label-picker-BdNAWuUX.js +1 -0
  123. package/core/built/admin/assets/{labels-GbxlXWtf.js → labels-Cyayfd-e.js} +1 -1
  124. package/core/built/admin/assets/layout-Bw4GnxXp.js +1 -0
  125. package/core/built/admin/assets/limit-service-9fwD33lU.js +20 -0
  126. package/core/built/admin/assets/link-helpers-tOlxmuH5.js +1 -0
  127. package/core/built/admin/assets/list-page-B6ANtzkd.js +1 -0
  128. package/core/built/admin/assets/mailchimp-Ds_NQ04w.svg +9 -0
  129. package/core/built/admin/assets/member-custom-fields-DvFyamLu.js +1 -0
  130. package/core/built/admin/assets/member-detail-Cg2vTqvJ.js +1 -0
  131. package/core/built/admin/assets/members-DsPxYAVm.js +10 -0
  132. package/core/built/admin/assets/modals-Bf_fyvHZ.js +31 -0
  133. package/core/built/admin/assets/moderation-Dlp7M5sa.js +1 -0
  134. package/core/built/admin/assets/multi-select-combobox-CH1o8XXQ.js +1 -0
  135. package/core/built/admin/assets/newsletter-CQ5CAYUI.js +1 -0
  136. package/core/built/admin/assets/{newsletters-DZm3yLj8.js → newsletters-D1jbtV-h.js} +1 -1
  137. package/core/built/admin/assets/note-BrAZ7Z0o.js +1 -0
  138. package/core/built/admin/assets/notifications-BnbHLOPC.js +1 -0
  139. package/core/built/admin/assets/nql-lang-B7faUf8D.js +19 -0
  140. package/core/built/admin/assets/{offers-D11QtPXX.js → offers-CrY6nSRR.js} +1 -1
  141. package/core/built/admin/assets/onboarding-route-eU146kuA.js +1 -0
  142. package/core/built/admin/assets/overview-BpbaK-xu.js +1 -0
  143. package/core/built/admin/assets/page-header-C0jVxtS8.js +1 -0
  144. package/core/built/admin/assets/{pages-BnObVRrf.js → pages-BbNWFcGj.js} +1 -1
  145. package/core/built/admin/assets/pintura-CXOmq-5H.svg +15 -0
  146. package/core/built/admin/assets/post-analytics-Iw7dk1mT.js +1 -0
  147. package/core/built/admin/assets/post-analytics-header-D9pJ4iNm.js +1 -0
  148. package/core/built/admin/assets/{post-analytics-provider-DSfBCtnj.js → post-analytics-provider-BDqcfBor.js} +1 -1
  149. package/core/built/admin/assets/post-share-modal-DBfKl2GZ.js +1 -0
  150. package/core/built/admin/assets/{posts-CwH8VJvB.js → posts-EWpZIjkr.js} +1 -1
  151. package/core/built/admin/assets/preferences-D4EANGoc.js +5 -0
  152. package/core/built/admin/assets/profile-dJ44Bn_W.js +1 -0
  153. package/core/built/admin/assets/{referrers-nxR52jPI.js → referrers-BCfFYSHc.js} +1 -1
  154. package/core/built/admin/assets/router-provider-C1R4AZ2o.js +1 -0
  155. package/core/built/admin/assets/select--2uVN9_-.js +1 -0
  156. package/core/built/admin/assets/settings-Bz3RZRmE.js +1 -0
  157. package/core/built/admin/assets/settings-SD9bxIpt.js +4 -0
  158. package/core/built/admin/assets/share-modal-CkdTTMJd.js +1 -0
  159. package/core/built/admin/assets/social-urls-BvFZwYcE.js +8 -0
  160. package/core/built/admin/assets/sort-button-DZp9Y2r1.js +1 -0
  161. package/core/built/admin/assets/source-icon-CHr9qJ2Y.js +1 -0
  162. package/core/built/admin/assets/stack-xiN0eQ6Z.js +1 -0
  163. package/core/built/admin/assets/stats-DGoXZxWE.js +1 -0
  164. package/core/built/admin/assets/stats-view-D2-1L0ms.js +1 -0
  165. package/core/built/admin/assets/step-1-RdahbwyG.js +1 -0
  166. package/core/built/admin/assets/step-2-D83PeRwd.js +1 -0
  167. package/core/built/admin/assets/step-3-p_Op4wv4.js +2 -0
  168. package/core/built/admin/assets/switch-qp518gFQ.js +1 -0
  169. package/core/built/admin/assets/table-B8LgTQJZ.js +1 -0
  170. package/core/built/admin/assets/tabs-D3ZOYkM7.js +1 -0
  171. package/core/built/admin/assets/tags-B4l891z2.js +1 -0
  172. package/core/built/admin/assets/text-DwtjwGkh.js +1 -0
  173. package/core/built/admin/assets/textarea-CTlb3Uu6.js +1 -0
  174. package/core/built/admin/assets/{tiers-EghDzCKc.js → tiers-BgYLWRJv.js} +1 -1
  175. package/core/built/admin/assets/toggle-group-DNXDkAwf.js +1 -0
  176. package/core/built/admin/assets/topic-filter-DZGjEcz3.js +1 -0
  177. package/core/built/admin/assets/{use-analytics-data-0l_OHtnR.js → use-analytics-data-CaT8RirX.js} +1 -1
  178. package/core/built/admin/assets/use-filter-params-DeQ8sX4N.js +1 -0
  179. package/core/built/admin/assets/use-growth-stats-9Db2CuJe.js +1 -0
  180. package/core/built/admin/assets/{use-post-referrers-DwI31Ril.js → use-post-referrers-BV54YcQS.js} +1 -1
  181. package/core/built/admin/assets/use-reply-chain-data-DuYZGDan.js +1 -0
  182. package/core/built/admin/assets/use-simple-pagination-8fTua9Nt.js +1 -0
  183. package/core/built/admin/assets/{use-tinybird-query-DvZBBMG7.js → use-tinybird-query-Dz156HvZ.js} +1 -1
  184. package/core/built/admin/assets/{use-tinybird-token-CQtebkvU.js → use-tinybird-token-C9RUdL_n.js} +1 -1
  185. package/core/built/admin/assets/{users-Pc8IRjk9.js → users-CcSCQFcQ.js} +1 -1
  186. package/core/built/admin/assets/virtual-list-BCvP0YF-.js +1 -0
  187. package/core/built/admin/assets/web-DTZnnUGr.js +1 -0
  188. package/core/built/admin/index.html +18 -22
  189. package/core/frontend/apps/private-blogging/index.js +1 -1
  190. package/core/frontend/apps/private-blogging/lib/middleware.js +1 -1
  191. package/core/frontend/helpers/split.js +2 -1
  192. package/core/frontend/meta/asset-url.js +1 -1
  193. package/core/frontend/meta/author-image.js +1 -1
  194. package/core/frontend/meta/blog-logo.js +1 -1
  195. package/core/frontend/meta/canonical-url.js +1 -1
  196. package/core/frontend/meta/cover-image.js +1 -1
  197. package/core/frontend/meta/get-meta.js +1 -1
  198. package/core/frontend/meta/og-image.js +1 -1
  199. package/core/frontend/meta/paginated-url.js +1 -1
  200. package/core/frontend/meta/twitter-image.js +1 -1
  201. package/core/frontend/meta/url.js +1 -1
  202. package/core/frontend/services/assets-minification/admin-auth-assets.js +1 -1
  203. package/core/frontend/services/llms/handler.js +1 -1
  204. package/core/frontend/services/proxy.js +31 -2
  205. package/core/frontend/services/routing/collection-router.js +1 -1
  206. package/core/frontend/services/routing/controllers/email-post.js +1 -1
  207. package/core/frontend/services/routing/controllers/entry/markdown.js +1 -1
  208. package/core/frontend/services/routing/controllers/entry/markdown.ts +1 -1
  209. package/core/frontend/services/routing/controllers/entry.js +1 -1
  210. package/core/frontend/services/routing/controllers/entry.ts +1 -1
  211. package/core/frontend/services/routing/controllers/previews.js +6 -7
  212. package/core/frontend/services/routing/controllers/unsubscribe.js +7 -3
  213. package/core/frontend/services/routing/email-router.js +1 -1
  214. package/core/frontend/services/routing/events.js +12 -0
  215. package/core/frontend/services/routing/middleware/page-param.js +1 -1
  216. package/core/frontend/services/routing/parent-router.js +1 -1
  217. package/core/frontend/services/routing/preview-router.js +1 -1
  218. package/core/frontend/services/routing/router-manager.js +4 -8
  219. package/core/frontend/services/routing/rss-router.js +1 -1
  220. package/core/frontend/services/routing/static-pages-router.js +1 -1
  221. package/core/frontend/services/routing/static-routes-router.js +1 -1
  222. package/core/frontend/services/routing/taxonomy-router.js +1 -1
  223. package/core/frontend/services/rss/generate-feed.js +1 -1
  224. package/core/frontend/services/sitemap/base-site-map-generator.js +23 -4
  225. package/core/frontend/services/sitemap/site-map-index-generator.js +1 -1
  226. package/core/frontend/services/sitemap/site-map-manager.js +14 -8
  227. package/core/frontend/services/sitemap/utils.js +1 -1
  228. package/core/frontend/services/theme-engine/middleware/update-global-template-options.js +1 -1
  229. package/core/frontend/services/theme-engine/middleware/update-local-template-options.js +1 -1
  230. package/core/frontend/utils/images.js +1 -1
  231. package/core/frontend/web/middleware/admin-toolbar.js +1 -1
  232. package/core/frontend/web/middleware/handle-image-sizes.js +2 -2
  233. package/core/frontend/web/middleware/redirect-ghost-to-admin.js +1 -1
  234. package/core/frontend/web/routers/serve-favicon.js +1 -1
  235. package/core/frontend/web/routers/serve-public-file.js +1 -1
  236. package/core/frontend/web/site.js +5 -5
  237. package/core/server/adapters/cache/AdapterCacheMemoryTTL.js +2 -2
  238. package/core/server/adapters/cache/MemoryCache.js +2 -2
  239. package/core/server/adapters/cache/Redis.js +1 -1
  240. package/core/server/adapters/cache/index.js +1 -1
  241. package/core/server/adapters/lib/redis/AdapterCacheRedis.js +2 -2
  242. package/core/server/adapters/route-settings/FileStore.js +2 -8
  243. package/core/server/adapters/route-settings/FileStore.ts +1 -6
  244. package/core/server/adapters/route-settings/S3RouteSettingsStore.js +206 -0
  245. package/core/server/adapters/route-settings/S3RouteSettingsStore.ts +199 -0
  246. package/core/server/adapters/route-settings/utils.js +13 -0
  247. package/core/server/adapters/route-settings/utils.ts +7 -0
  248. package/core/server/adapters/scheduling/deps.d.ts +0 -2
  249. package/core/server/adapters/scheduling/error-capture.js +76 -0
  250. package/core/server/adapters/scheduling/error-capture.ts +91 -0
  251. package/core/server/adapters/scheduling/utils.js +18 -34
  252. package/core/server/adapters/scheduling/utils.ts +36 -0
  253. package/core/server/adapters/storage/LocalFilesStorage.js +1 -1
  254. package/core/server/adapters/storage/LocalImagesStorage.js +1 -1
  255. package/core/server/adapters/storage/LocalMediaStorage.js +1 -1
  256. package/core/server/adapters/storage/LocalStorageBase.js +3 -3
  257. package/core/server/adapters/storage/S3Storage.js +2 -2
  258. package/core/server/adapters/storage/S3Storage.ts +1 -1
  259. package/core/server/adapters/storage/utils.js +1 -1
  260. package/core/server/api/endpoints/authentication.js +1 -1
  261. package/core/server/api/endpoints/featurebase.js +1 -1
  262. package/core/server/api/endpoints/files.js +2 -2
  263. package/core/server/api/endpoints/images.js +2 -2
  264. package/core/server/api/endpoints/libraries.d.ts +0 -1
  265. package/core/server/api/endpoints/media.js +8 -5
  266. package/core/server/api/endpoints/member-custom-fields.js +4 -1
  267. package/core/server/api/endpoints/member-custom-fields.ts +4 -1
  268. package/core/server/api/endpoints/posts.js +17 -1
  269. package/core/server/api/endpoints/search-index-public.js +4 -3
  270. package/core/server/api/endpoints/search-index.js +5 -4
  271. package/core/server/api/endpoints/settings-public.js +1 -1
  272. package/core/server/api/endpoints/settings.js +3 -1
  273. package/core/server/api/endpoints/utils/serializers/input/utils/url.js +9 -1
  274. package/core/server/api/endpoints/utils/serializers/output/default.js +7 -0
  275. package/core/server/api/endpoints/utils/serializers/output/files.js +1 -1
  276. package/core/server/api/endpoints/utils/serializers/output/media.js +1 -1
  277. package/core/server/api/endpoints/utils/serializers/output/member-custom-fields.js +7 -4
  278. package/core/server/api/endpoints/utils/serializers/output/member-custom-fields.ts +8 -4
  279. package/core/server/api/endpoints/utils/serializers/output/members.js +16 -26
  280. package/core/server/api/endpoints/utils/serializers/output/pages.js +7 -0
  281. package/core/server/api/endpoints/utils/serializers/output/posts.js +13 -0
  282. package/core/server/api/endpoints/utils/serializers/output/utils/url.js +1 -1
  283. package/core/server/api/endpoints/utils/validators/input/members.js +2 -52
  284. package/core/server/data/db/backup.js +1 -1
  285. package/core/server/data/importer/handlers/image.js +3 -3
  286. package/core/server/data/importer/import-manager.js +6 -4
  287. package/core/server/data/importer/importers/content-file-importer.js +2 -2
  288. package/core/server/data/migrations/utils/schema.js +4 -2
  289. package/core/server/data/migrations/versions/6.54/2026-07-19-16-59-00-add-content-import-permission.js +10 -0
  290. package/core/server/data/migrations/versions/6.54/2026-07-20-13-59-21-drop-email-tracked-sent-count-from-automation-action-revisions.js +7 -0
  291. package/core/server/data/migrations/versions/6.54/2026-07-21-01-58-20-add-automation-run-steps-poller-index.js +3 -0
  292. package/core/server/data/migrations/versions/6.54/2026-07-21-21-08-15-add-automated-email-recipients-mailgun-message-id-index.js +3 -0
  293. package/core/server/data/migrations/versions/6.54/2026-07-22-10-41-08-add-click-tracking-to-automated-email-recipients.js +13 -0
  294. package/core/server/data/migrations/versions/6.54/2026-07-22-15-10-30-add-email-clicked-count-to-automation-action-revisions.js +7 -0
  295. package/core/server/data/migrations/versions/6.54/2026-07-23-10-24-18-add-automation-action-revision-to-redirects.js +9 -0
  296. package/core/server/data/schema/commands.js +37 -3
  297. package/core/server/data/schema/fixtures/fixtures.json +7 -0
  298. package/core/server/data/schema/lib/default-index-name.js +20 -0
  299. package/core/server/data/schema/schema.js +40 -17
  300. package/core/server/data/seeders/data-generator.js +4 -2
  301. package/core/server/lib/image/image-size.js +5 -7
  302. package/core/server/lib/image/image-utils.js +2 -2
  303. package/core/server/lib/image/index.js +4 -4
  304. package/core/server/lib/lexical.js +3 -3
  305. package/core/server/lib/mobiledoc.js +2 -2
  306. package/core/server/lib/validate-password.js +1 -1
  307. package/core/server/models/action.js +9 -0
  308. package/core/server/models/base/plugins/generate-slug.js +1 -1
  309. package/core/server/models/collection.js +1 -1
  310. package/core/server/models/custom-theme-setting.js +1 -1
  311. package/core/server/models/member.js +73 -0
  312. package/core/server/models/newsletter.js +1 -1
  313. package/core/server/models/post.js +19 -5
  314. package/core/server/models/posts-meta.js +1 -1
  315. package/core/server/models/redirect.js +1 -1
  316. package/core/server/models/settings.js +1 -1
  317. package/core/server/models/snippet.js +1 -1
  318. package/core/server/models/tag.js +1 -1
  319. package/core/server/models/user.js +1 -1
  320. package/core/server/models/welcome-email-automated-email.js +1 -1
  321. package/core/server/services/activitypub/activity-pub-service-wrapper.js +1 -1
  322. package/core/server/services/adapter-manager/adapter-manager.js +5 -1
  323. package/core/server/services/adapter-manager/adapter-manager.ts +7 -2
  324. package/core/server/services/adapter-manager/index.js +18 -15
  325. package/core/server/services/adapter-manager/index.ts +23 -17
  326. package/core/server/services/adapter-manager/utils.js +124 -14
  327. package/core/server/services/adapter-manager/utils.ts +130 -14
  328. package/core/server/services/api-version-compatibility/mw-version-rewrites.js +1 -1
  329. package/core/server/services/audience-feedback/index.js +1 -1
  330. package/core/server/services/auth/passwordreset.js +1 -1
  331. package/core/server/services/auth/session/express-session.js +1 -1
  332. package/core/server/services/auth/session/index.js +2 -2
  333. package/core/server/services/auth/session/session-service.js +1 -1
  334. package/core/server/services/automations/automations-api.js +2 -19
  335. package/core/server/services/automations/automations-api.ts +2 -31
  336. package/core/server/services/automations/automations-repository.ts +15 -0
  337. package/core/server/services/automations/database-automations-repository.js +30 -2
  338. package/core/server/services/automations/database-automations-repository.ts +33 -2
  339. package/core/server/services/automations/poll.js +16 -12
  340. package/core/server/services/automations/poll.ts +17 -15
  341. package/core/server/services/automations/service.js +1 -1
  342. package/core/server/services/automations/service.ts +3 -13
  343. package/core/server/services/automations/welcome-email-automation-poll.js +2 -1
  344. package/core/server/services/comments/index.js +1 -1
  345. package/core/server/services/custom-redirects/index.js +2 -2
  346. package/core/server/services/email-analytics/email-analytics-service-wrapper.js +14 -13
  347. package/core/server/services/email-analytics/email-analytics-service.js +1 -1
  348. package/core/server/services/email-analytics/events/start-email-analytics-job-event.js +11 -15
  349. package/core/server/services/email-analytics/events/start-email-analytics-job-event.ts +15 -0
  350. package/core/server/services/email-analytics/index.js +2 -3
  351. package/core/server/services/email-analytics/index.ts +1 -2
  352. package/core/server/services/email-analytics/jobs/email-analytics-job-scheduler.js +1 -6
  353. package/core/server/services/email-analytics/jobs/email-analytics-job-scheduler.ts +0 -9
  354. package/core/server/services/email-analytics/jobs/fetch-latest/index.js +1 -1
  355. package/core/server/services/email-analytics/jobs/index.js +0 -2
  356. package/core/server/services/email-service/email-service-wrapper.js +1 -1
  357. package/core/server/services/email-service/email-service.js +20 -4
  358. package/core/server/services/explore-ping/explore-ping-service.js +24 -39
  359. package/core/server/services/explore-ping/explore-ping-service.ts +198 -0
  360. package/core/server/services/explore-ping/index.js +11 -11
  361. package/core/server/services/explore-ping/index.ts +42 -0
  362. package/core/server/services/gifts/gift-reminder-scheduler.js +1 -1
  363. package/core/server/services/gifts/gift-reminder-scheduler.ts +1 -1
  364. package/core/server/services/gifts/gift-service-wrapper.js +1 -1
  365. package/core/server/services/identity-tokens/identity-token-service-wrapper.js +1 -1
  366. package/core/server/services/indexnow-ping/constants.js +30 -0
  367. package/core/server/services/indexnow-ping/constants.ts +32 -0
  368. package/core/server/services/indexnow-ping/index.js +29 -253
  369. package/core/server/services/indexnow-ping/index.ts +37 -0
  370. package/core/server/services/indexnow-ping/indexnow-ping-service.js +229 -0
  371. package/core/server/services/indexnow-ping/indexnow-ping-service.ts +319 -0
  372. package/core/server/services/invites/index.js +1 -1
  373. package/core/server/services/lib/mailgun-client.js +3 -3
  374. package/core/server/services/link-redirection/index.js +2 -2
  375. package/core/server/services/link-redirection/link-redirect-repository.js +1 -1
  376. package/core/server/services/link-redirection/link-redirect.js +12 -13
  377. package/core/server/services/link-redirection/link-redirect.ts +22 -0
  378. package/core/server/services/link-redirection/link-redirects-service.js +1 -1
  379. package/core/server/services/link-tracking/full-post-link.js +2 -2
  380. package/core/server/services/link-tracking/index.js +1 -1
  381. package/core/server/services/mail/ghost-mailer.js +1 -1
  382. package/core/server/services/mail/index.js +1 -1
  383. package/core/server/services/media-inliner/external-media-inliner.js +2 -2
  384. package/core/server/services/media-inliner/service.js +4 -3
  385. package/core/server/services/member-attribution/index.js +1 -1
  386. package/core/server/services/member-welcome-emails/service.js +1 -1
  387. package/core/server/services/members/api.js +8 -2
  388. package/core/server/services/members/csv/index.js +10 -0
  389. package/core/server/services/members/csv/index.ts +3 -0
  390. package/core/server/services/members/csv/parse.js +94 -0
  391. package/core/server/services/members/csv/parse.ts +111 -0
  392. package/core/server/services/members/csv/types.js +2 -0
  393. package/core/server/services/members/csv/types.ts +39 -0
  394. package/core/server/services/members/csv/unparse.js +69 -0
  395. package/core/server/services/members/csv/unparse.ts +79 -0
  396. package/core/server/services/members/exporter/query.js +35 -8
  397. package/core/server/services/members/importer/config.js +35 -0
  398. package/core/server/services/members/importer/config.ts +39 -0
  399. package/core/server/services/members/importer/members-csv-importer.js +31 -13
  400. package/core/server/services/members/members-api/members-api.js +4 -2
  401. package/core/server/services/members/members-api/repositories/event-repository.js +13 -2
  402. package/core/server/services/members/members-api/services/member-bread-service.js +42 -19
  403. package/core/server/services/members/middleware.js +1 -1
  404. package/core/server/services/members/service.js +15 -2
  405. package/core/server/services/members/stripe-connect.js +1 -1
  406. package/core/server/services/members-custom-fields/actions.js +3 -0
  407. package/core/server/services/members-custom-fields/actions.ts +6 -2
  408. package/core/server/services/members-custom-fields/config.js +34 -0
  409. package/core/server/services/members-custom-fields/config.ts +38 -0
  410. package/core/server/services/members-custom-fields/definitions-service.js +156 -21
  411. package/core/server/services/members-custom-fields/definitions-service.ts +170 -25
  412. package/core/server/services/members-custom-fields/index.js +16 -3
  413. package/core/server/services/members-custom-fields/index.ts +17 -3
  414. package/core/server/services/members-custom-fields/values-service.js +56 -10
  415. package/core/server/services/members-custom-fields/values-service.ts +62 -10
  416. package/core/server/services/mentions/mention-sending-service.js +22 -2
  417. package/core/server/services/mentions/service.js +21 -5
  418. package/core/server/services/newsletters/index.js +1 -1
  419. package/core/server/services/oembed/oembed-service.js +122 -33
  420. package/core/server/services/oembed/service.js +4 -2
  421. package/core/server/services/offers/service.js +1 -1
  422. package/core/server/services/post-scheduling/index.js +4 -3
  423. package/core/server/services/post-scheduling/index.ts +4 -3
  424. package/core/server/services/post-scheduling/post-scheduling.js +1 -1
  425. package/core/server/services/post-scheduling/post-scheduling.ts +1 -1
  426. package/core/server/services/posts/post-email-handler.js +13 -6
  427. package/core/server/services/posts/post-scheduling.js +1 -1
  428. package/core/server/services/posts/posts-service-instance.js +1 -1
  429. package/core/server/services/posts/posts-service.js +2 -2
  430. package/core/server/services/posts-public/service.js +1 -1
  431. package/core/server/services/public-config/site.js +1 -1
  432. package/core/server/services/recommendations/recommendation-service-wrapper.js +1 -1
  433. package/core/server/services/recommendations/service/libraries.d.ts +0 -2
  434. package/core/server/services/route-settings/dynamic-routing-service.js +2 -3
  435. package/core/server/services/route-settings/index.js +1 -1
  436. package/core/server/services/route-settings/route-settings-parser.js +134 -124
  437. package/core/server/services/route-settings/route-settings-parser.ts +160 -125
  438. package/core/server/services/route-settings/validation-errors.js +178 -0
  439. package/core/server/services/route-settings/validation-errors.ts +196 -0
  440. package/core/server/services/settings/settings-service.js +2 -2
  441. package/core/server/services/settings-helpers/index.js +1 -1
  442. package/core/server/services/slack-notifications/service.js +1 -1
  443. package/core/server/services/slack-ping/constants.js +16 -0
  444. package/core/server/services/slack-ping/constants.ts +15 -0
  445. package/core/server/services/slack-ping/index.js +26 -204
  446. package/core/server/services/slack-ping/index.ts +35 -0
  447. package/core/server/services/slack-ping/slack-ping-service.js +214 -0
  448. package/core/server/services/slack-ping/slack-ping-service.ts +296 -0
  449. package/core/server/services/staff/index.js +1 -1
  450. package/core/server/services/stats/posts-stats-service.js +1 -1
  451. package/core/server/services/stats/service.js +1 -1
  452. package/core/server/services/stripe/service.js +1 -1
  453. package/core/server/services/tags-public/service.js +1 -1
  454. package/core/server/services/themes/validate.js +1 -1
  455. package/core/server/services/update-check/index.js +1 -1
  456. package/core/server/services/url/lazy-url-service.js +66 -6
  457. package/core/server/services/url/lazy-url-service.ts +73 -6
  458. package/core/server/services/url/url-generator.js +1 -1
  459. package/core/server/services/url/url-service-facade.js +68 -3
  460. package/core/server/services/url/url-service-facade.ts +79 -4
  461. package/core/server/services/url/url-service.js +1 -1
  462. package/core/server/services/url/urls.js +1 -1
  463. package/core/server/services/webhooks/index.js +19 -2
  464. package/core/server/services/webhooks/listen.js +6 -11
  465. package/core/server/services/webhooks/payload.js +1 -3
  466. package/core/server/services/webhooks/serialize.js +16 -1
  467. package/core/server/web/admin/app.js +1 -1
  468. package/core/server/web/admin/middleware/redirect-admin-urls.js +1 -1
  469. package/core/server/web/api/endpoints/admin/routes.js +1 -0
  470. package/core/server/web/api/middleware/cors.js +1 -1
  471. package/core/server/web/api/middleware/upload.js +4 -0
  472. package/core/server/web/gift-preview/controller.js +1 -1
  473. package/core/server/web/shared/middleware/redirect-amp-urls.js +1 -1
  474. package/core/server/web/shared/middleware/uncapitalise.js +2 -2
  475. package/core/server/web/shared/middleware/url-redirects.js +1 -1
  476. package/core/shared/config/defaults.json +8 -1
  477. package/core/shared/config/overrides.json +4 -0
  478. package/core/shared/url-utils.js +19 -15
  479. package/core/shared/url-utils.ts +20 -0
  480. package/ghost.js +0 -1
  481. package/package.json +26 -31
  482. package/pnpm-lock.yaml +681 -2789
  483. package/pnpm-workspace.yaml +31 -17
  484. package/components/tryghost-adapter-base-scheduling-0.0.0.tgz +0 -0
  485. package/components/tryghost-adapter-base-sso-0.0.0.tgz +0 -0
  486. package/core/built/admin/assets/Combination--5EZGaDs.js +0 -43
  487. package/core/built/admin/assets/Growth-C0NoKcHQ.js +0 -3
  488. package/core/built/admin/assets/Newsletters-Ed8kDjdy.js +0 -1
  489. package/core/built/admin/assets/Overview-CArPgRZt.js +0 -1
  490. package/core/built/admin/assets/Web-CjjYoVM3.js +0 -2
  491. package/core/built/admin/assets/account-migration-mfa5mNI9.js +0 -1
  492. package/core/built/admin/assets/activitypub/inbox-B3rjKMTv.js +0 -1221
  493. package/core/built/admin/assets/activitypub/layout-jcurg3rU.js +0 -1
  494. package/core/built/admin/assets/activitypub/use-activity-pub-queries-yqGRcnLn.js +0 -2569
  495. package/core/built/admin/assets/activitypub/users-CfyHIsTG.js +0 -1316
  496. package/core/built/admin/assets/automations-DkUnxNdv.js +0 -1
  497. package/core/built/admin/assets/automations-vjXofmV_.js +0 -1
  498. package/core/built/admin/assets/bluesky-sharing-T-pO3RcP.js +0 -1
  499. package/core/built/admin/assets/breadcrumb-CekPYTCn.js +0 -1
  500. package/core/built/admin/assets/button--i2uMDDn.js +0 -4
  501. package/core/built/admin/assets/card-CSWz_BXq.js +0 -1
  502. package/core/built/admin/assets/chart-helpers-C_4Ka_vT.js +0 -1
  503. package/core/built/admin/assets/chunk.524.8ce2c741473e2f0929dc.js +0 -25
  504. package/core/built/admin/assets/chunk.582.453459d6228d2955408b.js +0 -27
  505. package/core/built/admin/assets/code-editor-view-BfkwQto2.js +0 -1
  506. package/core/built/admin/assets/command-K75Jzs0G.js +0 -1
  507. package/core/built/admin/assets/comments-BEIi1DH-.js +0 -1
  508. package/core/built/admin/assets/container-BNXvlcjA.js +0 -1
  509. package/core/built/admin/assets/data-list-BmK5QMab.js +0 -1
  510. package/core/built/admin/assets/date-range-select-5sKzwbt7.js +0 -1
  511. package/core/built/admin/assets/disabled-sources-indicator-L_ssJTNr.js +0 -1
  512. package/core/built/admin/assets/dist-BBIJuyPg.js +0 -1
  513. package/core/built/admin/assets/dist-BHE0dp6o.js +0 -1
  514. package/core/built/admin/assets/dist-DJ6c4_De.js +0 -1
  515. package/core/built/admin/assets/dist-DW0QXQO3.js +0 -1
  516. package/core/built/admin/assets/dist-DxFLceO5.js +0 -1
  517. package/core/built/admin/assets/dist-QcACAOAD.js +0 -1
  518. package/core/built/admin/assets/dist-duxRDcQu.js +0 -177
  519. package/core/built/admin/assets/domain-CLaPZ9Rc.js +0 -1
  520. package/core/built/admin/assets/dropzone-E0bSX4_a.js +0 -5
  521. package/core/built/admin/assets/edit-profile-DqNPKaA4.js +0 -1
  522. package/core/built/admin/assets/editor-CkLXQDRp.js +0 -8
  523. package/core/built/admin/assets/empty-indicator-BTtOlgIo.js +0 -1
  524. package/core/built/admin/assets/esm-TSfGW7nz.js +0 -2
  525. package/core/built/admin/assets/explore-BEstLxKr.js +0 -1
  526. package/core/built/admin/assets/feed-C58p2voG.js +0 -1
  527. package/core/built/admin/assets/field-BSbDUHPv.js +0 -1
  528. package/core/built/admin/assets/filters-BgYv8Gt7.js +0 -1
  529. package/core/built/admin/assets/filters-DvenFkmK.js +0 -1
  530. package/core/built/admin/assets/framework-provider-jQktxm9P.js +0 -4
  531. package/core/built/admin/assets/ghost-dark-8590dea6cd695ff90f27d262c4464538.css +0 -1
  532. package/core/built/admin/assets/gift-link-modal-BPjXkEFt.js +0 -1
  533. package/core/built/admin/assets/growth-D4JNiVTe.js +0 -1
  534. package/core/built/admin/assets/hooks-Cjxexi_I.js +0 -2
  535. package/core/built/admin/assets/index-BacwlcOy.js +0 -67
  536. package/core/built/admin/assets/index-DxRNHH0o.css +0 -1
  537. package/core/built/admin/assets/inline-C22rnPro.js +0 -1
  538. package/core/built/admin/assets/input-group-CKfrILLP.js +0 -1
  539. package/core/built/admin/assets/jsx-runtime-CtMZ-PAS.js +0 -1
  540. package/core/built/admin/assets/kpi-card-DZxQKAIb.js +0 -1
  541. package/core/built/admin/assets/kpi-card-FidQ5zg3.js +0 -1
  542. package/core/built/admin/assets/kpi-helpers-C5T8Vaf1.js +0 -1
  543. package/core/built/admin/assets/kpi-tabs-_VRK6hGV.js +0 -1
  544. package/core/built/admin/assets/label-Cza7hPgn.js +0 -1
  545. package/core/built/admin/assets/label-picker-DgC_zLaw.js +0 -1
  546. package/core/built/admin/assets/layout-BxsXnKFX.js +0 -1
  547. package/core/built/admin/assets/limit-service-CTurluJQ.js +0 -20
  548. package/core/built/admin/assets/link-helpers-C3Gqy2cz.js +0 -1
  549. package/core/built/admin/assets/list-page-BtqCnow_.js +0 -1
  550. package/core/built/admin/assets/member-custom-fields-9uXFBqSt.js +0 -1
  551. package/core/built/admin/assets/member-detail-Dq6ZDIxo.js +0 -1
  552. package/core/built/admin/assets/members-CUWD9jal.js +0 -10
  553. package/core/built/admin/assets/modals-CIdwHVga.js +0 -31
  554. package/core/built/admin/assets/moderation-DTt04UDA.js +0 -1
  555. package/core/built/admin/assets/newsletter-cC8vcc9h.js +0 -1
  556. package/core/built/admin/assets/note-DZU0IY9e.js +0 -1
  557. package/core/built/admin/assets/notifications-NoHpBHJ4.js +0 -1
  558. package/core/built/admin/assets/nql-lang-CvCSOQG3.js +0 -19
  559. package/core/built/admin/assets/onboarding-route-Deg2hNm8.js +0 -1
  560. package/core/built/admin/assets/overview-BQPt_9w3.js +0 -1
  561. package/core/built/admin/assets/page-header-D6bEqPo2.js +0 -1
  562. package/core/built/admin/assets/post-analytics-header-BPw_B2_D.js +0 -1
  563. package/core/built/admin/assets/post-analytics-kp2_--XO.js +0 -1
  564. package/core/built/admin/assets/post-share-modal-Cave3Z4W.js +0 -1
  565. package/core/built/admin/assets/preferences-C7h3j_uy.js +0 -5
  566. package/core/built/admin/assets/profile-B2x4q2oB.js +0 -1
  567. package/core/built/admin/assets/router-provider-5fsX1CYn.js +0 -1
  568. package/core/built/admin/assets/select-PhPjIBBT.js +0 -1
  569. package/core/built/admin/assets/settings-D5P9Gwgo.js +0 -6
  570. package/core/built/admin/assets/settings-zUOzQV5I.js +0 -1
  571. package/core/built/admin/assets/share-modal-EfpfbuWF.js +0 -1
  572. package/core/built/admin/assets/social-urls-DgAF08Xc.js +0 -18
  573. package/core/built/admin/assets/sort-button-WOQYJm2H.js +0 -1
  574. package/core/built/admin/assets/source-icon-hm8Cj-Gx.js +0 -1
  575. package/core/built/admin/assets/stats-CnQDb62m.js +0 -1
  576. package/core/built/admin/assets/stats-view-CCfTJgSe.js +0 -1
  577. package/core/built/admin/assets/step-1-BGlhY4DS.js +0 -1
  578. package/core/built/admin/assets/step-2-D3sDEyFH.js +0 -1
  579. package/core/built/admin/assets/step-3-CvZoUdsc.js +0 -2
  580. package/core/built/admin/assets/switch-CNjkOmwz.js +0 -1
  581. package/core/built/admin/assets/table-C5nZYPnW.js +0 -1
  582. package/core/built/admin/assets/tabs-dtOcExye.js +0 -1
  583. package/core/built/admin/assets/tags-Dhm0FB_g.js +0 -1
  584. package/core/built/admin/assets/textarea-CeKw3wmV.js +0 -1
  585. package/core/built/admin/assets/toggle-group-DfhZIMTW.js +0 -1
  586. package/core/built/admin/assets/topic-filter-Cxe1Il41.js +0 -1
  587. package/core/built/admin/assets/typeof-Ca-duI-z.js +0 -1
  588. package/core/built/admin/assets/use-filter-params-Bub6BqNy.js +0 -1
  589. package/core/built/admin/assets/use-growth-stats-BJORomv0.js +0 -1
  590. package/core/built/admin/assets/use-reply-chain-data-B89YjVoo.js +0 -1
  591. package/core/built/admin/assets/use-simple-pagination-BL2SYXKi.js +0 -1
  592. package/core/built/admin/assets/virtual-list-DNJOSK9I.js +0 -1
  593. package/core/built/admin/assets/web-BHUE3YQc.js +0 -1
  594. package/core/server/adapters/scheduling/scheduling-base.js +0 -5
  595. package/core/server/adapters/sso/SSOBase.js +0 -5
  596. package/core/server/adapters/storage/index.js +0 -17
@@ -29,12 +29,35 @@ const MAX_KEY_LENGTH = columns.key.maxlength;
29
29
  const MAX_SLUG_ITERATIONS = 1000;
30
30
  // Reserve room for a `-<n>` suffix (n up to MAX_SLUG_ITERATIONS).
31
31
  const MAX_KEY_BASE_LENGTH = MAX_KEY_LENGTH - (String(MAX_SLUG_ITERATIONS).length + 1);
32
+ // A key becomes a property name on the plain objects that carry a member's values —
33
+ // on both sides of the wire, since `custom_fields` is JSON and a client gets a plain
34
+ // object from JSON.parse. A key naming a member of Object.prototype reads back as
35
+ // inherited rather than absent wherever one of those objects is indexed, and
36
+ // `__proto__` holds no value at all: the values schema drops it during parse, and
37
+ // assigning it sets a prototype rather than a property.
38
+ //
39
+ // Derived rather than listed, because the set is a consequence of how keys are
40
+ // minted: slugifying lowercases, so only an already-lowercase prototype name can
41
+ // survive to become a key. Currently `constructor` and `__proto__`.
42
+ const RESERVED_KEYS = Object.getOwnPropertyNames(Object.prototype)
43
+ .filter(name => slugify(name) === name);
32
44
  const FieldName = zod_1.z.string().trim().min(1, { message: 'Custom field name is required.' }).max(MAX_NAME_LENGTH, { message: 'Custom field name is too long.' });
33
45
  // The backend mints the key from the name, so create takes just a name and type.
34
46
  const AddFieldInput = zod_1.z.object({
35
47
  name: FieldName,
36
48
  type: custom_field_types_1.FieldTypeSchema
37
49
  });
50
+ // A bound on the work one request can ask for, separate from how many definitions
51
+ // a site may hold in total. Every definition in a batch costs several queries
52
+ // inside one open write transaction, so an operator raising the site ceiling must
53
+ // not also mean a single request can ask for an unbounded amount of that work.
54
+ const MAX_FIELDS_PER_REQUEST = 100;
55
+ // Create accepts a batch. The framework guarantees a non-empty array by the time a
56
+ // query runs, but the service validates the whole payload up front so a bad item
57
+ // anywhere fails the request before anything is written.
58
+ const AddFieldsInput = zod_1.z.array(AddFieldInput)
59
+ .min(1)
60
+ .max(MAX_FIELDS_PER_REQUEST, { message: `Custom fields can only be created ${MAX_FIELDS_PER_REQUEST} at a time.` });
38
61
  // Name and status are mutable. `key` and `type` are accepted so the immutability
39
62
  // rules can reject a change loudly; they are never persisted.
40
63
  const EditFieldInput = zod_1.z.object({
@@ -46,9 +69,16 @@ const EditFieldInput = zod_1.z.object({
46
69
  class CustomFieldDefinitionsService {
47
70
  knex;
48
71
  recordAction;
49
- constructor({ knex, recordAction }) {
72
+ getMaxDefinitions;
73
+ constructor({ knex, recordAction, getMaxDefinitions }) {
50
74
  this.knex = knex;
51
75
  this.recordAction = recordAction;
76
+ // A getter, not a value: the ceiling can be raised or lowered at any time,
77
+ // and a Ghost container holds no state across requests, so the limit that
78
+ // applies is whatever it resolves to when the request lands. Asking on
79
+ // every create means a change takes effect on the next one, with no
80
+ // restart. Where the number comes from is the caller's business.
81
+ this.getMaxDefinitions = getMaxDefinitions;
52
82
  }
53
83
  async browse(options = {}) {
54
84
  // Archived fields are hidden by default: most surfaces (member details, the
@@ -74,20 +104,54 @@ class CustomFieldDefinitionsService {
74
104
  }
75
105
  return zod_1.z.decode(codec_1.customFieldCodec, row);
76
106
  }
107
+ /**
108
+ * Create one or more field definitions. All-or-nothing: the batch runs in a
109
+ * single transaction, so a name clash or the operational cap on the third item
110
+ * leaves the first two unwritten rather than half-applying the request.
111
+ *
112
+ * Running inside the transaction also makes a batch self-consistent for free —
113
+ * `assertNameAvailable` and `mintKey` see the rows inserted earlier in the same
114
+ * batch, so two items sharing a name are caught and two items sharing a slug
115
+ * get distinct keys, exactly as if they had arrived as separate requests.
116
+ */
77
117
  async add(context, input) {
78
- const parsed = AddFieldInput.safeParse(input);
118
+ const requestedCount = Array.isArray(input) ? input.length : 0;
119
+ const parsed = AddFieldsInput.safeParse(input);
79
120
  if (!parsed.success) {
80
- throw new errors_1.default.ValidationError({ message: parsed.error.issues[0].message, property: parsed.error.issues[0].path[0]?.toString() });
81
- }
82
- const base = slugify(parsed.data.name);
83
- if (!base) {
84
- throw new errors_1.default.ValidationError({ message: 'Custom field name must contain at least one usable character.', property: 'name' });
121
+ const issue = parsed.error.issues[0];
122
+ throw new errors_1.default.ValidationError({
123
+ message: issue.message,
124
+ property: propertyOf(issue.path),
125
+ context: batchContext(issue.path[0], requestedCount)
126
+ });
85
127
  }
86
- await this.assertNameAvailable(parsed.data.name);
87
- const id = new bson_objectid_1.default().toHexString();
88
- const key = await this.mintKey(base);
128
+ const fields = parsed.data;
129
+ // Slugify before opening the transaction: it needs no database access, and
130
+ // an unusable name is a payload problem worth reporting on its own terms.
131
+ const bases = fields.map((field, index) => {
132
+ const base = slugify(field.name);
133
+ if (!base) {
134
+ throw new errors_1.default.ValidationError({
135
+ message: 'Custom field name must contain at least one usable character.',
136
+ property: 'name',
137
+ context: batchContext(index, requestedCount)
138
+ });
139
+ }
140
+ return base;
141
+ });
142
+ let created;
89
143
  try {
90
- await this.knex(TABLE).insert({ id, key, name: parsed.data.name, type: parsed.data.type, created_at: new Date() });
144
+ created = await this.knex.transaction(async (trx) => {
145
+ await this.assertWithinLimit(trx, fields.length);
146
+ const keys = [];
147
+ for (const [index, field] of fields.entries()) {
148
+ await this.assertNameAvailable(trx, field.name);
149
+ const key = await this.mintKey(trx, bases[index]);
150
+ await trx(TABLE).insert({ id: new bson_objectid_1.default().toHexString(), key, name: field.name, type: field.type, created_at: new Date() });
151
+ keys.push(key);
152
+ }
153
+ return this.readMany(trx, keys);
154
+ });
91
155
  }
92
156
  catch (err) {
93
157
  // mintKey already picked a free key, so a unique violation here only
@@ -98,17 +162,71 @@ class CustomFieldDefinitionsService {
98
162
  }
99
163
  throw err;
100
164
  }
101
- await this.recordAction({ context, verb: 'create', subject: key, details: { primary_name: parsed.data.name } });
102
- return this.read(key);
165
+ // Logged after the commit: the action log is a separate Bookshelf write
166
+ // outside this transaction, so recording inside it would leave orphaned
167
+ // "added" entries for fields a rollback never created.
168
+ for (const field of created) {
169
+ await this.recordAction({ context, verb: 'create', subject: field.id, details: { primary_name: field.name, key: field.key } });
170
+ }
171
+ return created;
172
+ }
173
+ /**
174
+ * The operational ceiling on how many definitions a site can hold. This is a
175
+ * safeguard against the database load unbounded definitions would create, not
176
+ * a pricing or packaging limit, so it applies wherever the feature is available
177
+ * and is deliberately not routed through the entitlement-driven limit service.
178
+ *
179
+ * Both active and archived definitions count: an archived field still occupies
180
+ * a row and still carries its members' values, so archiving alone frees no
181
+ * space. Deleting an archived field is what releases a slot.
182
+ *
183
+ * The count is a consistent read, not a locking one, so two creates landing at
184
+ * the same instant can both pass and take a site one over. That is deliberate:
185
+ * this is a ceiling on database load, and holding a table lock across every
186
+ * create to make it exact would cost more than the overshoot it prevents.
187
+ *
188
+ */
189
+ async assertWithinLimit(db, addedCount) {
190
+ const max = this.getMaxDefinitions();
191
+ const row = await db(TABLE).count({ count: '*' }).first();
192
+ const total = Number(row?.count ?? 0);
193
+ if (total + addedCount <= max) {
194
+ return;
195
+ }
196
+ // Two different situations reach here and they need different advice. At or
197
+ // over the ceiling there is nothing to do but free a slot. With slots still
198
+ // free the request was simply too big, and telling that operator to delete
199
+ // something is wrong: they have room, just not this much.
200
+ const remaining = max - total;
201
+ const advice = remaining > 0
202
+ ? `You can add ${remaining} more.`
203
+ : 'Delete a field you no longer need to make room.';
204
+ throw new errors_1.default.HostLimitError({
205
+ message: `Custom fields are limited to ${max} per site. ${advice}`,
206
+ code: 'CUSTOM_FIELDS_LIMIT_REACHED',
207
+ // `requested` is carried alongside the limit-service shape so a batch
208
+ // rejection is self-describing: without it a client sees free slots and
209
+ // a refusal, and has to re-derive its own payload size to explain why.
210
+ errorDetails: { limit: max, total, requested: addedCount }
211
+ });
212
+ }
213
+ /** Read back a batch in the order its keys were created, not the table's order. */
214
+ async readMany(db, keys) {
215
+ const rows = await db(TABLE).whereIn('key', keys).select('*');
216
+ const byKey = new Map(rows.map(row => [row.key, row]));
217
+ return keys.map(key => zod_1.z.decode(codec_1.customFieldCodec, byKey.get(key)));
103
218
  }
104
219
  /**
105
220
  * Pick a free key from the name's slug: `base`, then `base-2`, `base-3`, ...
106
221
  * Reads the keys already taken by that base — including archived fields, so a
107
222
  * slug is never reused once minted. Mirrors how tags/labels generate slugs.
108
223
  */
109
- async mintKey(base) {
224
+ async mintKey(db, base) {
110
225
  const safeBase = base.slice(0, MAX_KEY_BASE_LENGTH);
111
- const taken = new Set(await this.knex(TABLE).where('key', 'like', `${safeBase}%`).pluck('key'));
226
+ const taken = new Set([
227
+ ...RESERVED_KEYS,
228
+ ...await db(TABLE).where('key', 'like', `${safeBase}%`).pluck('key')
229
+ ]);
112
230
  if (!taken.has(safeBase)) {
113
231
  return safeBase;
114
232
  }
@@ -132,8 +250,8 @@ class CustomFieldDefinitionsService {
132
250
  * allow it in the SQLite test/dev suites. `exceptKey` lets a field keep its
133
251
  * own name on an unrelated edit.
134
252
  */
135
- async assertNameAvailable(name, exceptKey) {
136
- const query = this.knex(TABLE).whereRaw('LOWER(name) = ?', [name.toLowerCase()]);
253
+ async assertNameAvailable(db, name, exceptKey) {
254
+ const query = db(TABLE).whereRaw('LOWER(name) = ?', [name.toLowerCase()]);
137
255
  if (exceptKey) {
138
256
  query.whereNot('key', exceptKey);
139
257
  }
@@ -161,7 +279,7 @@ class CustomFieldDefinitionsService {
161
279
  // Only write (and log a rename) when the name actually changes, so
162
280
  // re-saving an unchanged field is a no-op rather than a spurious edit.
163
281
  if (patch.name !== undefined && patch.name !== existing.name) {
164
- await this.assertNameAvailable(patch.name, key);
282
+ await this.assertNameAvailable(this.knex, patch.name, key);
165
283
  try {
166
284
  await this.knex(TABLE)
167
285
  .where('key', key)
@@ -173,7 +291,7 @@ class CustomFieldDefinitionsService {
173
291
  }
174
292
  throw err;
175
293
  }
176
- await this.recordAction({ context, verb: 'rename', subject: key, details: { primary_name: patch.name, previous_name: existing.name } });
294
+ await this.recordAction({ context, verb: 'rename', subject: existing.id, details: { primary_name: patch.name, key, previous_name: existing.name } });
177
295
  }
178
296
  // A status change is the archive/restore transition. Only write (and log)
179
297
  // when it actually flips, so re-sending the current status is a no-op.
@@ -182,7 +300,7 @@ class CustomFieldDefinitionsService {
182
300
  .where('key', key)
183
301
  .update({ status: patch.status, updated_at: new Date() });
184
302
  const verb = patch.status === schema_1.FIELD_STATUS.archived ? 'archive' : 'restore';
185
- await this.recordAction({ context, verb, subject: key, details: { primary_name: patch.name ?? existing.name } });
303
+ await this.recordAction({ context, verb, subject: existing.id, details: { primary_name: patch.name ?? existing.name, key } });
186
304
  }
187
305
  return this.read(key);
188
306
  }
@@ -202,10 +320,27 @@ class CustomFieldDefinitionsService {
202
320
  throw new errors_1.default.ValidationError({ message: 'Only archived custom fields can be deleted. Archive the field first.' });
203
321
  }
204
322
  await this.knex(TABLE).where('key', key).del();
205
- await this.recordAction({ context, verb: 'delete', subject: key, details: { primary_name: field.name } });
323
+ await this.recordAction({ context, verb: 'delete', subject: field.id, details: { primary_name: field.name, key } });
206
324
  }
207
325
  }
208
326
  exports.CustomFieldDefinitionsService = CustomFieldDefinitionsService;
327
+ // The field a zod issue points at. Create validates an array, so an issue's path
328
+ // is prefixed with the item's index (`[0, 'name']`); `property` names the field
329
+ // that is wrong, so the numeric prefix is dropped. Which item it was is reported
330
+ // separately by batchContext, keeping `property` the bare field name a client can
331
+ // map straight onto its form input.
332
+ function propertyOf(path) {
333
+ return path.find(segment => typeof segment === 'string');
334
+ }
335
+ // Which definition of a batch an error belongs to. Only set when the request
336
+ // carried more than one: a lone definition needs no pointer, and every client
337
+ // today sends exactly one, so this stays absent on the common path.
338
+ function batchContext(index, requestedCount) {
339
+ if (requestedCount <= 1 || typeof index !== 'number') {
340
+ return undefined;
341
+ }
342
+ return `Custom field ${index + 1} of ${requestedCount}.`;
343
+ }
209
344
  function isUniqueConstraintViolation(error) {
210
345
  const code = error?.code;
211
346
  return code === 'ER_DUP_ENTRY' || code === 'SQLITE_CONSTRAINT';
@@ -32,6 +32,19 @@ const MAX_SLUG_ITERATIONS = 1000;
32
32
  // Reserve room for a `-<n>` suffix (n up to MAX_SLUG_ITERATIONS).
33
33
  const MAX_KEY_BASE_LENGTH = MAX_KEY_LENGTH - (String(MAX_SLUG_ITERATIONS).length + 1);
34
34
 
35
+ // A key becomes a property name on the plain objects that carry a member's values —
36
+ // on both sides of the wire, since `custom_fields` is JSON and a client gets a plain
37
+ // object from JSON.parse. A key naming a member of Object.prototype reads back as
38
+ // inherited rather than absent wherever one of those objects is indexed, and
39
+ // `__proto__` holds no value at all: the values schema drops it during parse, and
40
+ // assigning it sets a prototype rather than a property.
41
+ //
42
+ // Derived rather than listed, because the set is a consequence of how keys are
43
+ // minted: slugifying lowercases, so only an already-lowercase prototype name can
44
+ // survive to become a key. Currently `constructor` and `__proto__`.
45
+ const RESERVED_KEYS = Object.getOwnPropertyNames(Object.prototype)
46
+ .filter(name => slugify(name) === name);
47
+
35
48
  const FieldName = z.string().trim().min(1, {message: 'Custom field name is required.'}).max(MAX_NAME_LENGTH, {message: 'Custom field name is too long.'});
36
49
 
37
50
  // The backend mints the key from the name, so create takes just a name and type.
@@ -40,6 +53,19 @@ const AddFieldInput = z.object({
40
53
  type: FieldTypeSchema
41
54
  });
42
55
 
56
+ // A bound on the work one request can ask for, separate from how many definitions
57
+ // a site may hold in total. Every definition in a batch costs several queries
58
+ // inside one open write transaction, so an operator raising the site ceiling must
59
+ // not also mean a single request can ask for an unbounded amount of that work.
60
+ const MAX_FIELDS_PER_REQUEST = 100;
61
+
62
+ // Create accepts a batch. The framework guarantees a non-empty array by the time a
63
+ // query runs, but the service validates the whole payload up front so a bad item
64
+ // anywhere fails the request before anything is written.
65
+ const AddFieldsInput = z.array(AddFieldInput)
66
+ .min(1)
67
+ .max(MAX_FIELDS_PER_REQUEST, {message: `Custom fields can only be created ${MAX_FIELDS_PER_REQUEST} at a time.`});
68
+
43
69
  // Name and status are mutable. `key` and `type` are accepted so the immutability
44
70
  // rules can reject a change loudly; they are never persisted.
45
71
  const EditFieldInput = z.object({
@@ -52,10 +78,17 @@ const EditFieldInput = z.object({
52
78
  export class CustomFieldDefinitionsService {
53
79
  private knex: Knex;
54
80
  private recordAction: RecordCustomFieldAction;
81
+ private getMaxDefinitions: () => number;
55
82
 
56
- constructor({knex, recordAction}: {knex: Knex; recordAction: RecordCustomFieldAction}) {
83
+ constructor({knex, recordAction, getMaxDefinitions}: {knex: Knex; recordAction: RecordCustomFieldAction; getMaxDefinitions: () => number}) {
57
84
  this.knex = knex;
58
85
  this.recordAction = recordAction;
86
+ // A getter, not a value: the ceiling can be raised or lowered at any time,
87
+ // and a Ghost container holds no state across requests, so the limit that
88
+ // applies is whatever it resolves to when the request lands. Asking on
89
+ // every create means a change takes effect on the next one, with no
90
+ // restart. Where the number comes from is the caller's business.
91
+ this.getMaxDefinitions = getMaxDefinitions;
59
92
  }
60
93
 
61
94
  async browse(options: {filter?: string} = {}): Promise<CustomField[]> {
@@ -84,23 +117,58 @@ export class CustomFieldDefinitionsService {
84
117
  return z.decode(customFieldCodec, row);
85
118
  }
86
119
 
87
- async add(context: RequestContext, input: unknown): Promise<CustomField> {
88
- const parsed = AddFieldInput.safeParse(input);
89
- if (!parsed.success) {
90
- throw new errors.ValidationError({message: parsed.error.issues[0].message, property: parsed.error.issues[0].path[0]?.toString()});
91
- }
120
+ /**
121
+ * Create one or more field definitions. All-or-nothing: the batch runs in a
122
+ * single transaction, so a name clash or the operational cap on the third item
123
+ * leaves the first two unwritten rather than half-applying the request.
124
+ *
125
+ * Running inside the transaction also makes a batch self-consistent for free —
126
+ * `assertNameAvailable` and `mintKey` see the rows inserted earlier in the same
127
+ * batch, so two items sharing a name are caught and two items sharing a slug
128
+ * get distinct keys, exactly as if they had arrived as separate requests.
129
+ */
130
+ async add(context: RequestContext, input: unknown): Promise<CustomField[]> {
131
+ const requestedCount = Array.isArray(input) ? input.length : 0;
92
132
 
93
- const base = slugify(parsed.data.name);
94
- if (!base) {
95
- throw new errors.ValidationError({message: 'Custom field name must contain at least one usable character.', property: 'name'});
133
+ const parsed = AddFieldsInput.safeParse(input);
134
+ if (!parsed.success) {
135
+ const issue = parsed.error.issues[0];
136
+ throw new errors.ValidationError({
137
+ message: issue.message,
138
+ property: propertyOf(issue.path),
139
+ context: batchContext(issue.path[0], requestedCount)
140
+ });
96
141
  }
142
+ const fields = parsed.data;
97
143
 
98
- await this.assertNameAvailable(parsed.data.name);
144
+ // Slugify before opening the transaction: it needs no database access, and
145
+ // an unusable name is a payload problem worth reporting on its own terms.
146
+ const bases = fields.map((field, index) => {
147
+ const base = slugify(field.name);
148
+ if (!base) {
149
+ throw new errors.ValidationError({
150
+ message: 'Custom field name must contain at least one usable character.',
151
+ property: 'name',
152
+ context: batchContext(index, requestedCount)
153
+ });
154
+ }
155
+ return base;
156
+ });
99
157
 
100
- const id = new ObjectID().toHexString();
101
- const key = await this.mintKey(base);
158
+ let created: CustomField[];
102
159
  try {
103
- await this.knex(TABLE).insert({id, key, name: parsed.data.name, type: parsed.data.type, created_at: new Date()});
160
+ created = await this.knex.transaction(async (trx) => {
161
+ await this.assertWithinLimit(trx, fields.length);
162
+
163
+ const keys: string[] = [];
164
+ for (const [index, field] of fields.entries()) {
165
+ await this.assertNameAvailable(trx, field.name);
166
+ const key = await this.mintKey(trx, bases[index]);
167
+ await trx(TABLE).insert({id: new ObjectID().toHexString(), key, name: field.name, type: field.type, created_at: new Date()});
168
+ keys.push(key);
169
+ }
170
+ return this.readMany(trx, keys);
171
+ });
104
172
  } catch (err) {
105
173
  // mintKey already picked a free key, so a unique violation here only
106
174
  // means a concurrent create claimed the same key in between. The index
@@ -110,8 +178,65 @@ export class CustomFieldDefinitionsService {
110
178
  }
111
179
  throw err;
112
180
  }
113
- await this.recordAction({context, verb: 'create', subject: key, details: {primary_name: parsed.data.name}});
114
- return this.read(key);
181
+
182
+ // Logged after the commit: the action log is a separate Bookshelf write
183
+ // outside this transaction, so recording inside it would leave orphaned
184
+ // "added" entries for fields a rollback never created.
185
+ for (const field of created) {
186
+ await this.recordAction({context, verb: 'create', subject: field.id, details: {primary_name: field.name, key: field.key}});
187
+ }
188
+ return created;
189
+ }
190
+
191
+ /**
192
+ * The operational ceiling on how many definitions a site can hold. This is a
193
+ * safeguard against the database load unbounded definitions would create, not
194
+ * a pricing or packaging limit, so it applies wherever the feature is available
195
+ * and is deliberately not routed through the entitlement-driven limit service.
196
+ *
197
+ * Both active and archived definitions count: an archived field still occupies
198
+ * a row and still carries its members' values, so archiving alone frees no
199
+ * space. Deleting an archived field is what releases a slot.
200
+ *
201
+ * The count is a consistent read, not a locking one, so two creates landing at
202
+ * the same instant can both pass and take a site one over. That is deliberate:
203
+ * this is a ceiling on database load, and holding a table lock across every
204
+ * create to make it exact would cost more than the overshoot it prevents.
205
+ *
206
+ */
207
+ private async assertWithinLimit(db: Knex, addedCount: number): Promise<void> {
208
+ const max = this.getMaxDefinitions();
209
+
210
+ const row = await db(TABLE).count({count: '*'}).first();
211
+ const total = Number(row?.count ?? 0);
212
+ if (total + addedCount <= max) {
213
+ return;
214
+ }
215
+
216
+ // Two different situations reach here and they need different advice. At or
217
+ // over the ceiling there is nothing to do but free a slot. With slots still
218
+ // free the request was simply too big, and telling that operator to delete
219
+ // something is wrong: they have room, just not this much.
220
+ const remaining = max - total;
221
+ const advice = remaining > 0
222
+ ? `You can add ${remaining} more.`
223
+ : 'Delete a field you no longer need to make room.';
224
+
225
+ throw new errors.HostLimitError({
226
+ message: `Custom fields are limited to ${max} per site. ${advice}`,
227
+ code: 'CUSTOM_FIELDS_LIMIT_REACHED',
228
+ // `requested` is carried alongside the limit-service shape so a batch
229
+ // rejection is self-describing: without it a client sees free slots and
230
+ // a refusal, and has to re-derive its own payload size to explain why.
231
+ errorDetails: {limit: max, total, requested: addedCount}
232
+ });
233
+ }
234
+
235
+ /** Read back a batch in the order its keys were created, not the table's order. */
236
+ private async readMany(db: Knex, keys: string[]): Promise<CustomField[]> {
237
+ const rows = await db(TABLE).whereIn('key', keys).select('*');
238
+ const byKey = new Map(rows.map(row => [row.key, row]));
239
+ return keys.map(key => z.decode(customFieldCodec, byKey.get(key)!));
115
240
  }
116
241
 
117
242
  /**
@@ -119,11 +244,12 @@ export class CustomFieldDefinitionsService {
119
244
  * Reads the keys already taken by that base — including archived fields, so a
120
245
  * slug is never reused once minted. Mirrors how tags/labels generate slugs.
121
246
  */
122
- private async mintKey(base: string): Promise<string> {
247
+ private async mintKey(db: Knex, base: string): Promise<string> {
123
248
  const safeBase = base.slice(0, MAX_KEY_BASE_LENGTH);
124
- const taken = new Set(
125
- await this.knex(TABLE).where('key', 'like', `${safeBase}%`).pluck('key')
126
- );
249
+ const taken = new Set([
250
+ ...RESERVED_KEYS,
251
+ ...await db(TABLE).where('key', 'like', `${safeBase}%`).pluck('key')
252
+ ]);
127
253
  if (!taken.has(safeBase)) {
128
254
  return safeBase;
129
255
  }
@@ -148,8 +274,8 @@ export class CustomFieldDefinitionsService {
148
274
  * allow it in the SQLite test/dev suites. `exceptKey` lets a field keep its
149
275
  * own name on an unrelated edit.
150
276
  */
151
- private async assertNameAvailable(name: string, exceptKey?: string): Promise<void> {
152
- const query = this.knex(TABLE).whereRaw('LOWER(name) = ?', [name.toLowerCase()]);
277
+ private async assertNameAvailable(db: Knex, name: string, exceptKey?: string): Promise<void> {
278
+ const query = db(TABLE).whereRaw('LOWER(name) = ?', [name.toLowerCase()]);
153
279
  if (exceptKey) {
154
280
  query.whereNot('key', exceptKey);
155
281
  }
@@ -181,7 +307,7 @@ export class CustomFieldDefinitionsService {
181
307
  // Only write (and log a rename) when the name actually changes, so
182
308
  // re-saving an unchanged field is a no-op rather than a spurious edit.
183
309
  if (patch.name !== undefined && patch.name !== existing.name) {
184
- await this.assertNameAvailable(patch.name, key);
310
+ await this.assertNameAvailable(this.knex, patch.name, key);
185
311
  try {
186
312
  await this.knex(TABLE)
187
313
  .where('key', key)
@@ -192,7 +318,7 @@ export class CustomFieldDefinitionsService {
192
318
  }
193
319
  throw err;
194
320
  }
195
- await this.recordAction({context, verb: 'rename', subject: key, details: {primary_name: patch.name, previous_name: existing.name}});
321
+ await this.recordAction({context, verb: 'rename', subject: existing.id, details: {primary_name: patch.name, key, previous_name: existing.name}});
196
322
  }
197
323
 
198
324
  // A status change is the archive/restore transition. Only write (and log)
@@ -202,7 +328,7 @@ export class CustomFieldDefinitionsService {
202
328
  .where('key', key)
203
329
  .update({status: patch.status, updated_at: new Date()});
204
330
  const verb = patch.status === FIELD_STATUS.archived ? 'archive' : 'restore';
205
- await this.recordAction({context, verb, subject: key, details: {primary_name: patch.name ?? existing.name}});
331
+ await this.recordAction({context, verb, subject: existing.id, details: {primary_name: patch.name ?? existing.name, key}});
206
332
  }
207
333
 
208
334
  return this.read(key);
@@ -224,8 +350,27 @@ export class CustomFieldDefinitionsService {
224
350
  throw new errors.ValidationError({message: 'Only archived custom fields can be deleted. Archive the field first.'});
225
351
  }
226
352
  await this.knex(TABLE).where('key', key).del();
227
- await this.recordAction({context, verb: 'delete', subject: key, details: {primary_name: field.name}});
353
+ await this.recordAction({context, verb: 'delete', subject: field.id, details: {primary_name: field.name, key}});
354
+ }
355
+ }
356
+
357
+ // The field a zod issue points at. Create validates an array, so an issue's path
358
+ // is prefixed with the item's index (`[0, 'name']`); `property` names the field
359
+ // that is wrong, so the numeric prefix is dropped. Which item it was is reported
360
+ // separately by batchContext, keeping `property` the bare field name a client can
361
+ // map straight onto its form input.
362
+ function propertyOf(path: PropertyKey[]): string | undefined {
363
+ return path.find(segment => typeof segment === 'string');
364
+ }
365
+
366
+ // Which definition of a batch an error belongs to. Only set when the request
367
+ // carried more than one: a lone definition needs no pointer, and every client
368
+ // today sends exactly one, so this stays absent on the common path.
369
+ function batchContext(index: PropertyKey | undefined, requestedCount: number): string | undefined {
370
+ if (requestedCount <= 1 || typeof index !== 'number') {
371
+ return undefined;
228
372
  }
373
+ return `Custom field ${index + 1} of ${requestedCount}.`;
229
374
  }
230
375
 
231
376
  function isUniqueConstraintViolation(error: unknown): boolean {
@@ -5,6 +5,7 @@ exports.init = init;
5
5
  const definitions_service_1 = require("./definitions-service");
6
6
  const values_service_1 = require("./values-service");
7
7
  const actions_1 = require("./actions");
8
+ const config_1 = require("./config");
8
9
  function init() {
9
10
  // The two are constructed together below, so checking both keeps the "both or
10
11
  // neither" invariant explicit rather than trusting one to stand in for the pair.
@@ -14,8 +15,20 @@ function init() {
14
15
  const { knex } = require('../../data/db');
15
16
  const models = require('../../models');
16
17
  const recordAction = ({ context, verb, subject, details }) => (0, actions_1.recordCustomFieldAction)({ Action: models.Action, context, verb, subject, details });
17
- exports.definitions = new definitions_service_1.CustomFieldDefinitionsService({ knex, recordAction });
18
+ // Resolved here, not in the service: reading config is this module's job, and
19
+ // the service is handed a number. A getter rather than a value because the
20
+ // ceiling is an operator setting that can change between requests, and a Ghost
21
+ // container holds no state across them.
22
+ const config = require('../../../shared/config');
23
+ exports.definitions = new definitions_service_1.CustomFieldDefinitionsService({
24
+ knex,
25
+ recordAction,
26
+ getMaxDefinitions: () => (0, config_1.resolveMaxDefinitions)(config.get('members:customFields:maxDefinitions'))
27
+ });
18
28
  // The values service reads the field definitions straight from the table, so
19
- // it needs only knex — no handle on the definitions service.
20
- exports.values = new values_service_1.CustomFieldValuesService({ knex });
29
+ // it needs knex and the same ceiling — no handle on the definitions service.
30
+ exports.values = new values_service_1.CustomFieldValuesService({
31
+ knex,
32
+ getMaxDefinitions: () => (0, config_1.resolveMaxDefinitions)(config.get('members:customFields:maxDefinitions'))
33
+ });
21
34
  }
@@ -1,6 +1,7 @@
1
1
  import {CustomFieldDefinitionsService} from './definitions-service';
2
2
  import {CustomFieldValuesService} from './values-service';
3
3
  import {recordCustomFieldAction, type RecordCustomFieldAction} from './actions';
4
+ import {resolveMaxDefinitions} from './config';
4
5
 
5
6
  export type {CustomField} from './models';
6
7
  export type {RequestContext} from './actions';
@@ -29,8 +30,21 @@ export function init(): void {
29
30
  const recordAction: RecordCustomFieldAction = ({context, verb, subject, details}) =>
30
31
  recordCustomFieldAction({Action: models.Action, context, verb, subject, details});
31
32
 
32
- definitions = new CustomFieldDefinitionsService({knex, recordAction});
33
+ // Resolved here, not in the service: reading config is this module's job, and
34
+ // the service is handed a number. A getter rather than a value because the
35
+ // ceiling is an operator setting that can change between requests, and a Ghost
36
+ // container holds no state across them.
37
+ const config = require('../../../shared/config');
38
+
39
+ definitions = new CustomFieldDefinitionsService({
40
+ knex,
41
+ recordAction,
42
+ getMaxDefinitions: () => resolveMaxDefinitions(config.get('members:customFields:maxDefinitions'))
43
+ });
33
44
  // The values service reads the field definitions straight from the table, so
34
- // it needs only knex — no handle on the definitions service.
35
- values = new CustomFieldValuesService({knex});
45
+ // it needs knex and the same ceiling — no handle on the definitions service.
46
+ values = new CustomFieldValuesService({
47
+ knex,
48
+ getMaxDefinitions: () => resolveMaxDefinitions(config.get('members:customFields:maxDefinitions'))
49
+ });
36
50
  }