ghost 6.53.0 → 6.54.1
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.
- package/components/ghost-storage-base-3.0.0.tgz +0 -0
- package/components/tryghost-adapter-base-cache-0.2.0.tgz +0 -0
- package/components/tryghost-adapter-base-scheduling-0.2.0.tgz +0 -0
- package/components/tryghost-adapter-base-sso-0.1.1.tgz +0 -0
- package/components/tryghost-admin-api-schema-0.0.0.tgz +0 -0
- package/components/tryghost-custom-field-types-0.0.0.tgz +0 -0
- package/components/tryghost-i18n-0.0.0.tgz +0 -0
- package/components/tryghost-kg-converters-1.2.3.tgz +0 -0
- package/components/tryghost-kg-default-nodes-2.1.3.tgz +0 -0
- package/components/tryghost-kg-lexical-html-renderer-1.4.3.tgz +0 -0
- package/core/boot.js +9 -6
- package/core/built/admin/assets/Combination-CJG7Flp-.js +41 -0
- package/core/built/admin/assets/Growth-BTP1LthE.js +3 -0
- package/core/built/admin/assets/Newsletters-CSEX0myc.js +1 -0
- package/core/built/admin/assets/Overview-BjACEsJF.js +1 -0
- package/core/built/admin/assets/{PolarAngleAxis-pciQ0MOy.js → PolarAngleAxis-BGjf7sBu.js} +1 -1
- package/core/built/admin/assets/{RadialBarChart-DWmw2PcX.js → RadialBarChart-zeFf9mBz.js} +1 -1
- package/core/built/admin/assets/Web-CxxxjCcv.js +2 -0
- package/core/built/admin/assets/account-migration-BklF2I_I.js +1 -0
- package/core/built/admin/assets/activitypub/{account-migration-BPZG37DJ.js → account-migration-CNovr6W-.js} +13 -13
- package/core/built/admin/assets/activitypub/activitypub.js +8 -8
- package/core/built/admin/assets/activitypub/{avatar-ZYco0ITb.js → avatar-B3DEDB1l.js} +10 -10
- package/core/built/admin/assets/activitypub/{avatar-flipboard-D541lc4F.js → avatar-flipboard-Cqrg0XMx.js} +3 -3
- package/core/built/admin/assets/activitypub/{bluesky-sharing-NOHUtHjl.js → bluesky-sharing-Ceslb1_M.js} +38 -37
- package/core/built/admin/assets/activitypub/{chunk-KS7C4IRE-B-GOFDPZ.js → chunk-KS7C4IRE-D4HEcEwi.js} +716 -710
- package/core/built/admin/assets/activitypub/{content-formatters-bwrp4iEO.js → content-formatters-BpQARh28.js} +229 -181
- package/core/built/admin/assets/activitypub/{copy-C7m7ovHw.js → copy-D9UWIAan.js} +2 -2
- package/core/built/admin/assets/activitypub/{dist-BqkDUQxJ2.js → dist-ByFmDSNP2.js} +64 -63
- package/core/built/admin/assets/activitypub/{dist-B5ira32t.js → dist-DHAjBcV3.js} +5 -5
- package/core/built/admin/assets/activitypub/{domain-iqE4YXwD.js → domain-C1qQvJ8C.js} +33 -33
- package/core/built/admin/assets/activitypub/{edit-profile-B8MFYh28.js → edit-profile-BcH-jEfM.js} +46 -45
- package/core/built/admin/assets/activitypub/{explore-BbdKodp3.js → explore-AxwBh9dI.js} +20 -20
- package/core/built/admin/assets/activitypub/{feed-Cl3A5owG.js → feed-BaOlbh7a.js} +15 -15
- package/core/built/admin/assets/activitypub/{field-DL6WyR-c.js → field-B7mYYQpN.js} +13 -13
- package/core/built/admin/assets/activitypub/{hash-6G3DzwL9.js → hash-BC9Q0N4c.js} +2 -2
- package/core/built/admin/assets/activitypub/{inbox-YmhuBice.js → inbox-DQ4ZnC1m.js} +2 -2
- package/core/built/admin/assets/activitypub/inbox-vXez-5uJ.js +1221 -0
- package/core/built/admin/assets/activitypub/{label-DGbLjDvE.js → label-Chv5T8bw.js} +6 -6
- package/core/built/admin/assets/activitypub/layout-CziktJXo.js +1 -0
- package/core/built/admin/assets/activitypub/{moderation-qnBtk1zS.js → moderation-DtSM2i1I.js} +32 -31
- package/core/built/admin/assets/activitypub/{note-C7ZRoSJv.js → note-Dckp4S-2.js} +21 -21
- package/core/built/admin/assets/activitypub/{notifications-DiEA4b1o.js → notifications-CG5T22vg.js} +17 -17
- package/core/built/admin/assets/activitypub/{onboarding-_JVgEwgV.js → onboarding-BFDkzeR2.js} +2 -2
- package/core/built/admin/assets/activitypub/{onboarding-DG4zu3qM.js → onboarding-D6rcBdPe.js} +5 -5
- package/core/built/admin/assets/activitypub/{preferences-B3FtocnP.js → preferences-D_799jdl.js} +1607 -1606
- package/core/built/admin/assets/activitypub/{profile-D8ZJfYXB.js → profile-D2JfXBQl.js} +264 -263
- package/core/built/admin/assets/activitypub/{reply-C1vggenw.js → reply-CpdMeAO5.js} +2 -2
- package/core/built/admin/assets/activitypub/{routes-BOii-V9n.js → routes-CCHe2dQp.js} +1317 -1298
- package/core/built/admin/assets/activitypub/{separator-1gNokN5S.js → separator-BI1FLsYM.js} +4 -4
- package/core/built/admin/assets/activitypub/{settings--9Sb0nbl.js → settings-kx-9Enkz.js} +14 -14
- package/core/built/admin/assets/activitypub/{site-fkMmqv0V.js → site-x4_IjVJ0.js} +10 -10
- package/core/built/admin/assets/activitypub/{step-1-QhivSMw2.js → step-1-DIDg4aHb.js} +15 -15
- package/core/built/admin/assets/activitypub/{step-2-pMk9YwqD.js → step-2-BfGe8BYt.js} +7 -7
- package/core/built/admin/assets/activitypub/{step-3-DvmVC1fo.js → step-3-mE9spAqw.js} +12 -12
- package/core/built/admin/assets/activitypub/{tabs-Dc0FEEYD.js → tabs-CyPRQQ1N.js} +241 -237
- package/core/built/admin/assets/activitypub/{topic-filter-CuDyM5bD.js → topic-filter-DLHBZT93.js} +4 -4
- package/core/built/admin/assets/activitypub/use-activity-pub-queries-VzbN99u_.js +1957 -0
- package/core/built/admin/assets/activitypub/{use-navigate-with-base-path-Drx1kdAK.js → use-navigate-with-base-path-CAwXTmqH.js} +165 -175
- package/core/built/admin/assets/activitypub/{use-reply-chain-data-BHkCHcW5.js → use-reply-chain-data-BAB6fNGt.js} +8 -8
- package/core/built/admin/assets/activitypub/users-BLeiOTHc.js +1464 -0
- package/core/built/admin/assets/activitypub/{x-BHCEe-c2.js → x-C8tAnXly.js} +2 -2
- package/core/built/admin/assets/{audience-adiFehTD.js → audience-X_XsZivI.js} +1 -1
- package/core/built/admin/assets/{automated-emails-UWM02CMr.js → automated-emails-of1MvVsv.js} +1 -1
- package/core/built/admin/assets/automations-B7-y3dXt.js +1 -0
- package/core/built/admin/assets/automations-DRFjW5fw.js +1 -0
- package/core/built/admin/assets/{avatar-flipboard-Lz3F9fNT.js → avatar-flipboard-C6Kj3Awu.js} +1 -1
- package/core/built/admin/assets/bluesky-sharing-BbbjWdzs.js +1 -0
- package/core/built/admin/assets/box-BWbhXh21.js +1 -0
- package/core/built/admin/assets/breadcrumb-B-M9Ip_2.js +1 -0
- package/core/built/admin/assets/card-DGoDJzrn.js +1 -0
- package/core/built/admin/assets/{chart-ChpW-NzQ.js → chart-BuRT2Y6p.js} +14 -14
- package/core/built/admin/assets/chart-helpers-xiSsW8r0.js +1 -0
- package/core/built/admin/assets/checkbox-EhJKHFdZ.js +1 -0
- package/core/built/admin/assets/{chunk.432.c2caa0b20f03e71d1a35.js → chunk.211.6f7328d63c41536451b3.js} +2025 -2819
- package/core/built/admin/assets/{chunk.432.c2caa0b20f03e71d1a35.js.LICENSE.txt → chunk.211.6f7328d63c41536451b3.js.LICENSE.txt} +1 -62
- package/core/built/admin/assets/{chunk.831.0703a12e49f51cec3f3a.js → chunk.361.3cdb584b973959e52ccc.js} +78 -85
- package/core/built/admin/assets/chunk.524.8365cd9280405c0f6216.js +25 -0
- package/core/built/admin/assets/chunk.582.61d078937e6102769f2c.js +27 -0
- package/core/built/admin/assets/code-editor-view-CZtJFGhR.js +1 -0
- package/core/built/admin/assets/comments-CvyXxOyN.js +1 -0
- package/core/built/admin/assets/container-BYz2Ed39.js +1 -0
- package/core/built/admin/assets/data-list-16rkJYYi.js +1 -0
- package/core/built/admin/assets/date-range-select-CTlyo8zW.js +1 -0
- package/core/built/admin/assets/disabled-sources-indicator-qs2GfLvl.js +1 -0
- package/core/built/admin/assets/{dist-DJ6c4_De.js → dist-2Tnz2Yfq.js} +1 -1
- package/core/built/admin/assets/{dist-CsekE4FL.js → dist-CQJY6SVY.js} +1 -1
- package/core/built/admin/assets/dist-CjY-f5Z1.js +1 -0
- package/core/built/admin/assets/dist-DGzPJ_bO.js +4 -0
- package/core/built/admin/assets/dist-DIC8yz5C.js +1 -0
- package/core/built/admin/assets/{dist-BbGrW05X.js → dist-DyViJwIy.js} +1 -1
- package/core/built/admin/assets/dist-Ndy-PuGN.js +1 -0
- package/core/built/admin/assets/dist-P_ohUlTi.js +1 -0
- package/core/built/admin/assets/domain-CPNXKVOV.js +1 -0
- package/core/built/admin/assets/dropzone-BmCXnw6J.js +5 -0
- package/core/built/admin/assets/edit-profile-SGrJve5M.js +1 -0
- package/core/built/admin/assets/editor-BOG2YPUm.js +8 -0
- package/core/built/admin/assets/empty-indicator-oX1u1Pkn.js +1 -0
- package/core/built/admin/assets/esm-CFX93H5M.js +2 -0
- package/core/built/admin/assets/explore-CUYLv9G9.js +1 -0
- package/core/built/admin/assets/feed-CPdJMttL.js +1 -0
- package/core/built/admin/assets/field-D0XISCK2.js +1 -0
- package/core/built/admin/assets/{filter-sources-v39ihhhV.js → filter-sources-Bieps75g.js} +1 -1
- package/core/built/admin/assets/filters-CuB-A7ma.js +1 -0
- package/core/built/admin/assets/filters-bXkkWro8.js +1 -0
- package/core/built/admin/assets/framework-provider-CmtnPXIT.js +4 -0
- package/core/built/admin/assets/{ghost-b57f077997b739a8ae9a70772dc016cb.js → ghost-05f9a7182ed56903dba05878a5b8422b.js} +46 -41
- package/core/built/admin/assets/ghost-dark-c8d0ab7f515b2010c173e23e9b32ea8a.css +1 -0
- package/core/built/admin/assets/gift-link-modal-su8MsRdg.js +1 -0
- package/core/built/admin/assets/{gift-links-CY3GwiVi.js → gift-links-nBxJjZiQ.js} +1 -1
- package/core/built/admin/assets/growth-Ba6S5FQX.js +1 -0
- package/core/built/admin/assets/hooks-BApwwBE0.js +2 -0
- package/core/built/admin/assets/{inbox-TEISAmth.js → inbox-Xx1CdnfO.js} +38 -38
- package/core/built/admin/assets/index-D8PbzuT9.css +1 -0
- package/core/built/admin/assets/index-ukW_0Uie.js +67 -0
- package/core/built/admin/assets/inline-EMAm3okk.js +1 -0
- package/core/built/admin/assets/input-group-CGOXcy4l.js +1 -0
- package/core/built/admin/assets/input-surface-BpAjZXY5.js +1 -0
- package/core/built/admin/assets/{react-dom-C6aoWSfl.js → jsx-runtime-DX3WBWVO.js} +1 -1
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +5 -2
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +2 -2
- package/core/built/admin/assets/{koenig-lexical-Di7b48ED.js → koenig-lexical-B61IALf5.js} +2 -2
- package/core/built/admin/assets/kpi-card-BlZaG1Lo.js +1 -0
- package/core/built/admin/assets/kpi-card-DCdlyaSS.js +1 -0
- package/core/built/admin/assets/kpi-helpers-DpAuS1Nb.js +1 -0
- package/core/built/admin/assets/kpi-tabs-Dme44bQS.js +1 -0
- package/core/built/admin/assets/label-dSs5aepE.js +1 -0
- package/core/built/admin/assets/label-picker-Uq91atex.js +1 -0
- package/core/built/admin/assets/{labels-GbxlXWtf.js → labels-Cyayfd-e.js} +1 -1
- package/core/built/admin/assets/layout-WUcz6tu1.js +1 -0
- package/core/built/admin/assets/limit-service-9fwD33lU.js +20 -0
- package/core/built/admin/assets/link-helpers-tOlxmuH5.js +1 -0
- package/core/built/admin/assets/list-page-B6ANtzkd.js +1 -0
- package/core/built/admin/assets/mailchimp-Ds_NQ04w.svg +9 -0
- package/core/built/admin/assets/member-custom-fields-NCzrMuba.js +1 -0
- package/core/built/admin/assets/member-detail-DPWDtYnN.js +1 -0
- package/core/built/admin/assets/members-B9rO7qr4.js +10 -0
- package/core/built/admin/assets/modals-CxRaan1J.js +31 -0
- package/core/built/admin/assets/moderation-D8zon60E.js +1 -0
- package/core/built/admin/assets/multi-select-combobox-1wYm3nle.js +1 -0
- package/core/built/admin/assets/newsletter-DbYMRxRS.js +1 -0
- package/core/built/admin/assets/{newsletters-DZm3yLj8.js → newsletters-D1jbtV-h.js} +1 -1
- package/core/built/admin/assets/note-PiWPnnrh.js +1 -0
- package/core/built/admin/assets/notifications-DEgqqB9G.js +1 -0
- package/core/built/admin/assets/nql-lang-B7faUf8D.js +19 -0
- package/core/built/admin/assets/{offers-D11QtPXX.js → offers-CrY6nSRR.js} +1 -1
- package/core/built/admin/assets/onboarding-route-BzF6HeVn.js +1 -0
- package/core/built/admin/assets/overview-DixLeIqY.js +1 -0
- package/core/built/admin/assets/page-header-op0eN36I.js +1 -0
- package/core/built/admin/assets/{pages-BnObVRrf.js → pages-BbNWFcGj.js} +1 -1
- package/core/built/admin/assets/pintura-CXOmq-5H.svg +15 -0
- package/core/built/admin/assets/post-analytics-WwRDEnrV.js +1 -0
- package/core/built/admin/assets/post-analytics-header-DsMoLx0R.js +1 -0
- package/core/built/admin/assets/{post-analytics-provider-DSfBCtnj.js → post-analytics-provider-BJCPLMIb.js} +1 -1
- package/core/built/admin/assets/post-share-modal-Dx-MEjhI.js +1 -0
- package/core/built/admin/assets/{posts-CwH8VJvB.js → posts-EWpZIjkr.js} +1 -1
- package/core/built/admin/assets/preferences-D7-j6A4C.js +5 -0
- package/core/built/admin/assets/profile-Dl69QPp0.js +1 -0
- package/core/built/admin/assets/{referrers-nxR52jPI.js → referrers-BCfFYSHc.js} +1 -1
- package/core/built/admin/assets/router-provider-C1R4AZ2o.js +1 -0
- package/core/built/admin/assets/select-C1qDZnW4.js +1 -0
- package/core/built/admin/assets/settings-BbmC_Iqq.js +4 -0
- package/core/built/admin/assets/settings-H6HxFgfI.js +1 -0
- package/core/built/admin/assets/shade-app-B3heOAfz-CGXQwWeO.js +3 -0
- package/core/built/admin/assets/share-modal-BJCadJyp.js +1 -0
- package/core/built/admin/assets/social-urls-BPL1nADp.js +8 -0
- package/core/built/admin/assets/sort-button-DyYlfYp7.js +1 -0
- package/core/built/admin/assets/source-icon-_ubRDbg_.js +1 -0
- package/core/built/admin/assets/stack-xiN0eQ6Z.js +1 -0
- package/core/built/admin/assets/stats-BQZMGMvn.js +1 -0
- package/core/built/admin/assets/stats-view-Doei-qCX.js +1 -0
- package/core/built/admin/assets/step-1-IKiLBPoC.js +1 -0
- package/core/built/admin/assets/step-2-jJ41PsDN.js +1 -0
- package/core/built/admin/assets/step-3-CaqFAEqf.js +2 -0
- package/core/built/admin/assets/switch-t-WyENU2.js +1 -0
- package/core/built/admin/assets/table-DxUL78jU.js +1 -0
- package/core/built/admin/assets/tabs-BQcJQ1ke.js +1 -0
- package/core/built/admin/assets/tags-CNujGwkT.js +1 -0
- package/core/built/admin/assets/text-DwtjwGkh.js +1 -0
- package/core/built/admin/assets/textarea-Bjccvh_E.js +1 -0
- package/core/built/admin/assets/{tiers-EghDzCKc.js → tiers-BgYLWRJv.js} +1 -1
- package/core/built/admin/assets/toggle-group-BeEpQ69p.js +1 -0
- package/core/built/admin/assets/topic-filter-Bb5MOD-1.js +1 -0
- package/core/built/admin/assets/{use-analytics-data-0l_OHtnR.js → use-analytics-data-Cds6XDjg.js} +1 -1
- package/core/built/admin/assets/use-filter-params-DjXkhO54.js +1 -0
- package/core/built/admin/assets/use-growth-stats-D8uf98-G.js +1 -0
- package/core/built/admin/assets/{use-post-referrers-DwI31Ril.js → use-post-referrers-CRtgyuXB.js} +1 -1
- package/core/built/admin/assets/use-reply-chain-data-DbKYX50v.js +1 -0
- package/core/built/admin/assets/use-simple-pagination-DC01kpo0.js +1 -0
- package/core/built/admin/assets/{use-tinybird-query-DvZBBMG7.js → use-tinybird-query-C7bQRJWw.js} +1 -1
- package/core/built/admin/assets/{use-tinybird-token-CQtebkvU.js → use-tinybird-token-Az4tam7_.js} +1 -1
- package/core/built/admin/assets/{users-Pc8IRjk9.js → users-CcSCQFcQ.js} +1 -1
- package/core/built/admin/assets/virtual-list-Dhuh9fGx.js +1 -0
- package/core/built/admin/assets/web-LtHNiNBV.js +1 -0
- package/core/built/admin/index.html +22 -22
- package/core/frontend/apps/private-blogging/index.js +1 -1
- package/core/frontend/apps/private-blogging/lib/middleware.js +1 -1
- package/core/frontend/helpers/split.js +2 -1
- package/core/frontend/meta/asset-url.js +1 -1
- package/core/frontend/meta/author-image.js +1 -1
- package/core/frontend/meta/blog-logo.js +1 -1
- package/core/frontend/meta/canonical-url.js +1 -1
- package/core/frontend/meta/cover-image.js +1 -1
- package/core/frontend/meta/get-meta.js +1 -1
- package/core/frontend/meta/og-image.js +1 -1
- package/core/frontend/meta/paginated-url.js +1 -1
- package/core/frontend/meta/twitter-image.js +1 -1
- package/core/frontend/meta/url.js +1 -1
- package/core/frontend/services/assets-minification/admin-auth-assets.js +1 -1
- package/core/frontend/services/llms/handler.js +1 -1
- package/core/frontend/services/proxy.js +42 -3
- package/core/frontend/services/routing/collection-router.js +1 -1
- package/core/frontend/services/routing/controllers/email-post.js +1 -1
- package/core/frontend/services/routing/controllers/entry/markdown.js +1 -1
- package/core/frontend/services/routing/controllers/entry/markdown.ts +1 -1
- package/core/frontend/services/routing/controllers/entry.js +1 -1
- package/core/frontend/services/routing/controllers/entry.ts +1 -1
- package/core/frontend/services/routing/controllers/previews.js +6 -7
- package/core/frontend/services/routing/controllers/unsubscribe.js +7 -3
- package/core/frontend/services/routing/email-router.js +1 -1
- package/core/frontend/services/routing/events.js +12 -0
- package/core/frontend/services/routing/middleware/page-param.js +1 -1
- package/core/frontend/services/routing/parent-router.js +1 -1
- package/core/frontend/services/routing/preview-router.js +1 -1
- package/core/frontend/services/routing/router-manager.js +4 -8
- package/core/frontend/services/routing/rss-router.js +1 -1
- package/core/frontend/services/routing/static-pages-router.js +1 -1
- package/core/frontend/services/routing/static-routes-router.js +1 -1
- package/core/frontend/services/routing/taxonomy-router.js +1 -1
- package/core/frontend/services/rss/generate-feed.js +1 -1
- package/core/frontend/services/sitemap/base-site-map-generator.js +23 -4
- package/core/frontend/services/sitemap/site-map-index-generator.js +1 -1
- package/core/frontend/services/sitemap/site-map-manager.js +14 -8
- package/core/frontend/services/sitemap/utils.js +1 -1
- package/core/frontend/services/theme-engine/middleware/update-global-template-options.js +1 -1
- package/core/frontend/services/theme-engine/middleware/update-local-template-options.js +1 -1
- package/core/frontend/utils/images.js +1 -1
- package/core/frontend/web/middleware/admin-toolbar.js +1 -1
- package/core/frontend/web/middleware/handle-image-sizes.js +2 -2
- package/core/frontend/web/middleware/redirect-ghost-to-admin.js +1 -1
- package/core/frontend/web/routers/serve-favicon.js +1 -1
- package/core/frontend/web/routers/serve-public-file.js +1 -1
- package/core/frontend/web/site.js +5 -5
- package/core/server/adapters/cache/AdapterCacheMemoryTTL.js +2 -2
- package/core/server/adapters/cache/MemoryCache.js +2 -2
- package/core/server/adapters/cache/Redis.js +1 -1
- package/core/server/adapters/cache/index.js +1 -1
- package/core/server/adapters/lib/redis/AdapterCacheRedis.js +2 -2
- package/core/server/adapters/route-settings/FileStore.js +2 -8
- package/core/server/adapters/route-settings/FileStore.ts +1 -6
- package/core/server/adapters/route-settings/S3RouteSettingsStore.js +206 -0
- package/core/server/adapters/route-settings/S3RouteSettingsStore.ts +199 -0
- package/core/server/adapters/route-settings/utils.js +13 -0
- package/core/server/adapters/route-settings/utils.ts +7 -0
- package/core/server/adapters/scheduling/deps.d.ts +0 -2
- package/core/server/adapters/scheduling/error-capture.js +76 -0
- package/core/server/adapters/scheduling/error-capture.ts +91 -0
- package/core/server/adapters/scheduling/utils.js +18 -34
- package/core/server/adapters/scheduling/utils.ts +36 -0
- package/core/server/adapters/storage/LocalFilesStorage.js +13 -11
- package/core/server/adapters/storage/LocalFilesStorage.ts +24 -0
- package/core/server/adapters/storage/LocalImagesStorage.js +14 -12
- package/core/server/adapters/storage/LocalImagesStorage.ts +24 -0
- package/core/server/adapters/storage/LocalMediaStorage.js +13 -11
- package/core/server/adapters/storage/LocalMediaStorage.ts +24 -0
- package/core/server/adapters/storage/LocalStorageBase.js +107 -167
- package/core/server/adapters/storage/LocalStorageBase.ts +342 -0
- package/core/server/adapters/storage/S3Storage.js +2 -2
- package/core/server/adapters/storage/S3Storage.ts +1 -1
- package/core/server/adapters/storage/utils.js +36 -50
- package/core/server/adapters/storage/utils.ts +60 -0
- package/core/server/api/endpoints/authentication.js +1 -1
- package/core/server/api/endpoints/db.js +6 -0
- package/core/server/api/endpoints/featurebase.js +1 -1
- package/core/server/api/endpoints/files.js +2 -2
- package/core/server/api/endpoints/images.js +2 -2
- package/core/server/api/endpoints/libraries.d.ts +0 -1
- package/core/server/api/endpoints/media.js +8 -5
- package/core/server/api/endpoints/member-custom-fields.js +4 -1
- package/core/server/api/endpoints/member-custom-fields.ts +4 -1
- package/core/server/api/endpoints/members.js +25 -28
- package/core/server/api/endpoints/pages.js +6 -1
- package/core/server/api/endpoints/posts.js +28 -3
- package/core/server/api/endpoints/search-index-public.js +4 -3
- package/core/server/api/endpoints/search-index.js +5 -4
- package/core/server/api/endpoints/settings-public.js +1 -1
- package/core/server/api/endpoints/settings.js +3 -1
- package/core/server/api/endpoints/utils/serializers/input/utils/url.js +9 -1
- package/core/server/api/endpoints/utils/serializers/output/default.js +7 -0
- package/core/server/api/endpoints/utils/serializers/output/files.js +1 -1
- package/core/server/api/endpoints/utils/serializers/output/mappers/comments.js +4 -1
- package/core/server/api/endpoints/utils/serializers/output/media.js +1 -1
- package/core/server/api/endpoints/utils/serializers/output/member-custom-fields.js +7 -4
- package/core/server/api/endpoints/utils/serializers/output/member-custom-fields.ts +8 -4
- package/core/server/api/endpoints/utils/serializers/output/members.js +27 -106
- package/core/server/api/endpoints/utils/serializers/output/pages.js +7 -0
- package/core/server/api/endpoints/utils/serializers/output/posts.js +13 -0
- package/core/server/api/endpoints/utils/serializers/output/utils/url.js +1 -1
- package/core/server/api/endpoints/utils/validators/input/members.js +2 -52
- package/core/server/data/db/backup.js +1 -1
- package/core/server/data/exporter/export-filename.js +2 -1
- package/core/server/data/importer/handlers/image.js +3 -3
- package/core/server/data/importer/import-manager.js +6 -4
- package/core/server/data/importer/importers/content-file-importer.js +2 -2
- package/core/server/data/importer/importers/data/revue-subscriber-importer.js +6 -10
- package/core/server/data/importer/importers/json-to-html.js +24 -4
- package/core/server/data/migrations/utils/schema.js +4 -2
- package/core/server/data/migrations/versions/6.54/2026-07-19-16-59-00-add-content-import-permission.js +10 -0
- 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
- package/core/server/data/migrations/versions/6.54/2026-07-21-01-58-20-add-automation-run-steps-poller-index.js +3 -0
- 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
- package/core/server/data/migrations/versions/6.54/2026-07-22-10-41-08-add-click-tracking-to-automated-email-recipients.js +13 -0
- 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
- package/core/server/data/migrations/versions/6.54/2026-07-23-10-24-18-add-automation-action-revision-to-redirects.js +9 -0
- package/core/server/data/schema/commands.js +37 -3
- package/core/server/data/schema/fixtures/fixtures.json +7 -0
- package/core/server/data/schema/lib/default-index-name.js +20 -0
- package/core/server/data/schema/schema.js +40 -17
- package/core/server/data/seeders/data-generator.js +4 -2
- package/core/server/lib/image/image-size.js +7 -23
- package/core/server/lib/image/image-utils.js +16 -2
- package/core/server/lib/image/index.js +4 -4
- package/core/server/lib/lexical.js +3 -3
- package/core/server/lib/mobiledoc.js +2 -2
- package/core/server/lib/request-external.js +1 -1
- package/core/server/lib/validate-password.js +1 -1
- package/core/server/models/action.js +9 -0
- package/core/server/models/base/plugins/generate-slug.js +1 -1
- package/core/server/models/collection.js +1 -1
- package/core/server/models/custom-theme-setting.js +1 -1
- package/core/server/models/member.js +73 -0
- package/core/server/models/newsletter.js +1 -1
- package/core/server/models/post.js +19 -5
- package/core/server/models/posts-meta.js +1 -1
- package/core/server/models/redirect.js +1 -1
- package/core/server/models/settings.js +1 -1
- package/core/server/models/snippet.js +1 -1
- package/core/server/models/tag.js +1 -1
- package/core/server/models/user.js +47 -71
- package/core/server/models/welcome-email-automated-email.js +1 -1
- package/core/server/services/activitypub/activity-pub-service-wrapper.js +1 -1
- package/core/server/services/adapter-manager/adapter-manager.js +5 -1
- package/core/server/services/adapter-manager/adapter-manager.ts +7 -2
- package/core/server/services/adapter-manager/index.js +18 -15
- package/core/server/services/adapter-manager/index.ts +23 -17
- package/core/server/services/adapter-manager/utils.js +124 -14
- package/core/server/services/adapter-manager/utils.ts +130 -14
- package/core/server/services/api-version-compatibility/mw-version-rewrites.js +1 -1
- package/core/server/services/audience-feedback/index.js +1 -1
- package/core/server/services/auth/passwordreset.js +1 -1
- package/core/server/services/auth/session/express-session.js +1 -1
- package/core/server/services/auth/session/index.js +2 -2
- package/core/server/services/auth/session/session-service.js +29 -2
- package/core/server/services/automations/automations-api.js +8 -25
- package/core/server/services/automations/automations-api.ts +8 -37
- package/core/server/services/automations/automations-repository.ts +16 -0
- package/core/server/services/automations/database-automations-repository.js +37 -4
- package/core/server/services/automations/database-automations-repository.ts +41 -4
- package/core/server/services/automations/poll.js +16 -12
- package/core/server/services/automations/poll.ts +17 -15
- package/core/server/services/automations/service.js +1 -1
- package/core/server/services/automations/service.ts +3 -13
- package/core/server/services/automations/welcome-email-automation-poll.js +2 -1
- package/core/server/services/comments/comments-service-emails.js +4 -1
- package/core/server/services/comments/index.js +1 -1
- package/core/server/services/custom-redirects/index.js +2 -2
- package/core/server/services/email-analytics/email-analytics-service-wrapper.js +14 -13
- package/core/server/services/email-analytics/email-analytics-service.js +1 -1
- package/core/server/services/email-analytics/events/start-email-analytics-job-event.js +11 -15
- package/core/server/services/email-analytics/events/start-email-analytics-job-event.ts +15 -0
- package/core/server/services/email-analytics/index.js +2 -3
- package/core/server/services/email-analytics/index.ts +1 -2
- package/core/server/services/email-analytics/jobs/email-analytics-job-scheduler.js +1 -6
- package/core/server/services/email-analytics/jobs/email-analytics-job-scheduler.ts +0 -9
- package/core/server/services/email-analytics/jobs/fetch-latest/index.js +1 -1
- package/core/server/services/email-analytics/jobs/index.js +0 -2
- package/core/server/services/email-service/email-service-wrapper.js +1 -1
- package/core/server/services/email-service/email-service.js +20 -4
- package/core/server/services/explore-ping/explore-ping-service.js +24 -39
- package/core/server/services/explore-ping/explore-ping-service.ts +198 -0
- package/core/server/services/explore-ping/index.js +11 -11
- package/core/server/services/explore-ping/index.ts +42 -0
- package/core/server/services/gifts/gift-reminder-scheduler.js +1 -1
- package/core/server/services/gifts/gift-reminder-scheduler.ts +1 -1
- package/core/server/services/gifts/gift-service-wrapper.js +1 -1
- package/core/server/services/identity-tokens/identity-token-service-wrapper.js +1 -1
- package/core/server/services/indexnow-ping/constants.js +30 -0
- package/core/server/services/indexnow-ping/constants.ts +32 -0
- package/core/server/services/indexnow-ping/index.js +29 -253
- package/core/server/services/indexnow-ping/index.ts +37 -0
- package/core/server/services/indexnow-ping/indexnow-ping-service.js +229 -0
- package/core/server/services/indexnow-ping/indexnow-ping-service.ts +319 -0
- package/core/server/services/invites/index.js +1 -1
- package/core/server/services/lib/mailgun-client.js +3 -3
- package/core/server/services/link-redirection/index.js +2 -2
- package/core/server/services/link-redirection/link-redirect-repository.js +1 -1
- package/core/server/services/link-redirection/link-redirect.js +12 -13
- package/core/server/services/link-redirection/link-redirect.ts +22 -0
- package/core/server/services/link-redirection/link-redirects-service.js +1 -1
- package/core/server/services/link-tracking/full-post-link.js +2 -2
- package/core/server/services/link-tracking/index.js +1 -1
- package/core/server/services/mail/ghost-mailer.js +1 -1
- package/core/server/services/mail/index.js +1 -1
- package/core/server/services/media-inliner/external-media-inliner.js +2 -2
- package/core/server/services/media-inliner/service.js +4 -3
- package/core/server/services/member-attribution/index.js +1 -1
- package/core/server/services/member-welcome-emails/service.js +1 -1
- package/core/server/services/members/api.js +8 -2
- package/core/server/services/members/import-export/config.js +30 -0
- package/core/server/services/members/import-export/config.ts +34 -0
- package/core/server/services/members/import-export/csv/index.js +10 -0
- package/core/server/services/members/import-export/csv/index.ts +2 -0
- package/core/server/services/members/import-export/csv/parse.js +64 -0
- package/core/server/services/members/import-export/csv/parse.ts +69 -0
- package/core/server/services/members/import-export/csv/serialize.js +13 -0
- package/core/server/services/members/import-export/csv/serialize.ts +18 -0
- package/core/server/services/members/import-export/export/exporter.js +202 -0
- package/core/server/services/members/import-export/export/exporter.ts +325 -0
- package/core/server/services/members/import-export/import/completion-email.js +65 -0
- package/core/server/services/members/import-export/import/completion-email.ts +119 -0
- package/core/server/services/members/import-export/import/importer.js +276 -0
- package/core/server/services/members/import-export/import/importer.ts +430 -0
- package/core/server/services/members/import-export/import/reader.js +32 -0
- package/core/server/services/members/import-export/import/reader.ts +33 -0
- package/core/server/services/members/import-export/import/row.js +33 -0
- package/core/server/services/members/import-export/import/row.ts +52 -0
- package/core/server/services/members/import-export/import/spool.js +30 -0
- package/core/server/services/members/import-export/import/spool.ts +38 -0
- package/core/server/services/members/import-export/index.js +90 -0
- package/core/server/services/members/import-export/index.ts +118 -0
- package/core/server/services/members/members-api/controllers/router-controller.js +7 -0
- package/core/server/services/members/members-api/members-api.js +4 -2
- package/core/server/services/members/members-api/repositories/event-repository.js +13 -2
- package/core/server/services/members/members-api/services/member-bread-service.js +42 -19
- package/core/server/services/members/middleware.js +1 -1
- package/core/server/services/members/service.js +28 -17
- package/core/server/services/members/stripe-connect.js +1 -1
- package/core/server/services/members-custom-fields/actions.js +3 -0
- package/core/server/services/members-custom-fields/actions.ts +6 -2
- package/core/server/services/members-custom-fields/config.js +34 -0
- package/core/server/services/members-custom-fields/config.ts +38 -0
- package/core/server/services/members-custom-fields/definitions-service.js +156 -21
- package/core/server/services/members-custom-fields/definitions-service.ts +170 -25
- package/core/server/services/members-custom-fields/index.js +16 -3
- package/core/server/services/members-custom-fields/index.ts +17 -3
- package/core/server/services/members-custom-fields/values-service.js +56 -10
- package/core/server/services/members-custom-fields/values-service.ts +62 -10
- package/core/server/services/mentions/mention-sending-service.js +22 -2
- package/core/server/services/mentions/service.js +21 -5
- package/core/server/services/newsletters/index.js +1 -1
- package/core/server/services/oembed/oembed-service.js +122 -33
- package/core/server/services/oembed/service.js +4 -2
- package/core/server/services/offers/service.js +1 -1
- package/core/server/services/post-scheduling/index.js +4 -3
- package/core/server/services/post-scheduling/index.ts +4 -3
- package/core/server/services/post-scheduling/post-scheduling.js +1 -1
- package/core/server/services/post-scheduling/post-scheduling.ts +1 -1
- package/core/server/services/posts/post-email-handler.js +13 -6
- package/core/server/services/posts/post-scheduling.js +1 -1
- package/core/server/services/posts/posts-exporter.js +6 -4
- package/core/server/services/posts/posts-service-instance.js +1 -1
- package/core/server/services/posts/posts-service.js +4 -4
- package/core/server/services/posts-public/service.js +1 -1
- package/core/server/services/public-config/site.js +1 -1
- package/core/server/services/recommendations/recommendation-service-wrapper.js +1 -1
- package/core/server/services/recommendations/service/libraries.d.ts +0 -2
- package/core/server/services/route-settings/dynamic-routing-service.js +2 -3
- package/core/server/services/route-settings/index.js +1 -1
- package/core/server/services/route-settings/route-settings-parser.js +134 -124
- package/core/server/services/route-settings/route-settings-parser.ts +160 -125
- package/core/server/services/route-settings/validation-errors.js +178 -0
- package/core/server/services/route-settings/validation-errors.ts +196 -0
- package/core/server/services/settings/settings-service.js +2 -2
- package/core/server/services/settings-helpers/index.js +1 -1
- package/core/server/services/slack-notifications/service.js +1 -1
- package/core/server/services/slack-ping/constants.js +16 -0
- package/core/server/services/slack-ping/constants.ts +15 -0
- package/core/server/services/slack-ping/index.js +26 -204
- package/core/server/services/slack-ping/index.ts +35 -0
- package/core/server/services/slack-ping/slack-ping-service.js +214 -0
- package/core/server/services/slack-ping/slack-ping-service.ts +296 -0
- package/core/server/services/staff/index.js +1 -1
- package/core/server/services/stats/posts-stats-service.js +1 -1
- package/core/server/services/stats/service.js +1 -1
- package/core/server/services/stripe/service.js +1 -1
- package/core/server/services/tags-public/service.js +1 -1
- package/core/server/services/themes/storage.js +9 -0
- package/core/server/services/themes/theme-storage.js +1 -1
- package/core/server/services/themes/validate.js +1 -1
- package/core/server/services/update-check/index.js +1 -1
- package/core/server/services/url/lazy-url-service.js +66 -6
- package/core/server/services/url/lazy-url-service.ts +73 -6
- package/core/server/services/url/url-generator.js +1 -1
- package/core/server/services/url/url-service-facade.js +68 -3
- package/core/server/services/url/url-service-facade.ts +79 -4
- package/core/server/services/url/url-service.js +1 -1
- package/core/server/services/url/urls.js +1 -1
- package/core/server/services/webhooks/index.js +19 -2
- package/core/server/services/webhooks/listen.js +6 -11
- package/core/server/services/webhooks/payload.js +1 -3
- package/core/server/services/webhooks/serialize.js +16 -1
- package/core/server/web/admin/app.js +1 -1
- package/core/server/web/admin/middleware/redirect-admin-urls.js +1 -1
- package/core/server/web/api/endpoints/admin/routes.js +1 -0
- package/core/server/web/api/middleware/cors.js +1 -1
- package/core/server/web/api/middleware/upload.js +4 -0
- package/core/server/web/gift-preview/controller.js +1 -1
- package/core/server/web/shared/middleware/redirect-amp-urls.js +1 -1
- package/core/server/web/shared/middleware/uncapitalise.js +2 -2
- package/core/server/web/shared/middleware/url-redirects.js +1 -1
- package/core/shared/config/defaults.json +10 -2
- package/core/shared/config/overrides.json +4 -0
- package/core/shared/express.js +6 -1
- package/core/shared/labs.js +2 -1
- package/core/shared/url-utils.js +19 -15
- package/core/shared/url-utils.ts +20 -0
- package/ghost.js +0 -1
- package/package.json +30 -35
- package/pnpm-lock.yaml +681 -2789
- package/pnpm-workspace.yaml +31 -17
- package/components/tryghost-adapter-base-scheduling-0.0.0.tgz +0 -0
- package/components/tryghost-adapter-base-sso-0.0.0.tgz +0 -0
- package/core/built/admin/assets/Combination--5EZGaDs.js +0 -43
- package/core/built/admin/assets/Growth-C0NoKcHQ.js +0 -3
- package/core/built/admin/assets/Newsletters-Ed8kDjdy.js +0 -1
- package/core/built/admin/assets/Overview-CArPgRZt.js +0 -1
- package/core/built/admin/assets/Web-CjjYoVM3.js +0 -2
- package/core/built/admin/assets/account-migration-mfa5mNI9.js +0 -1
- package/core/built/admin/assets/activitypub/inbox-B3rjKMTv.js +0 -1221
- package/core/built/admin/assets/activitypub/layout-jcurg3rU.js +0 -1
- package/core/built/admin/assets/activitypub/use-activity-pub-queries-yqGRcnLn.js +0 -2569
- package/core/built/admin/assets/activitypub/users-CfyHIsTG.js +0 -1316
- package/core/built/admin/assets/automations-DkUnxNdv.js +0 -1
- package/core/built/admin/assets/automations-vjXofmV_.js +0 -1
- package/core/built/admin/assets/bluesky-sharing-T-pO3RcP.js +0 -1
- package/core/built/admin/assets/breadcrumb-CekPYTCn.js +0 -1
- package/core/built/admin/assets/button--i2uMDDn.js +0 -4
- package/core/built/admin/assets/card-CSWz_BXq.js +0 -1
- package/core/built/admin/assets/chart-helpers-C_4Ka_vT.js +0 -1
- package/core/built/admin/assets/chunk.524.8ce2c741473e2f0929dc.js +0 -25
- package/core/built/admin/assets/chunk.582.453459d6228d2955408b.js +0 -27
- package/core/built/admin/assets/code-editor-view-BfkwQto2.js +0 -1
- package/core/built/admin/assets/command-K75Jzs0G.js +0 -1
- package/core/built/admin/assets/comments-BEIi1DH-.js +0 -1
- package/core/built/admin/assets/container-BNXvlcjA.js +0 -1
- package/core/built/admin/assets/data-list-BmK5QMab.js +0 -1
- package/core/built/admin/assets/date-range-select-5sKzwbt7.js +0 -1
- package/core/built/admin/assets/disabled-sources-indicator-L_ssJTNr.js +0 -1
- package/core/built/admin/assets/dist-BBIJuyPg.js +0 -1
- package/core/built/admin/assets/dist-BHE0dp6o.js +0 -1
- package/core/built/admin/assets/dist-CT46_f_J.js +0 -1
- package/core/built/admin/assets/dist-DW0QXQO3.js +0 -1
- package/core/built/admin/assets/dist-DxFLceO5.js +0 -1
- package/core/built/admin/assets/dist-QcACAOAD.js +0 -1
- package/core/built/admin/assets/dist-duxRDcQu.js +0 -177
- package/core/built/admin/assets/domain-CLaPZ9Rc.js +0 -1
- package/core/built/admin/assets/dropzone-E0bSX4_a.js +0 -5
- package/core/built/admin/assets/edit-profile-DqNPKaA4.js +0 -1
- package/core/built/admin/assets/editor-CkLXQDRp.js +0 -8
- package/core/built/admin/assets/empty-indicator-BTtOlgIo.js +0 -1
- package/core/built/admin/assets/esm-TSfGW7nz.js +0 -2
- package/core/built/admin/assets/explore-BEstLxKr.js +0 -1
- package/core/built/admin/assets/feed-C58p2voG.js +0 -1
- package/core/built/admin/assets/field-BSbDUHPv.js +0 -1
- package/core/built/admin/assets/filters-BgYv8Gt7.js +0 -1
- package/core/built/admin/assets/filters-DvenFkmK.js +0 -1
- package/core/built/admin/assets/framework-provider-jQktxm9P.js +0 -4
- package/core/built/admin/assets/ghost-dark-8590dea6cd695ff90f27d262c4464538.css +0 -1
- package/core/built/admin/assets/gift-link-modal-BPjXkEFt.js +0 -1
- package/core/built/admin/assets/growth-D4JNiVTe.js +0 -1
- package/core/built/admin/assets/hooks-Cjxexi_I.js +0 -2
- package/core/built/admin/assets/index-BacwlcOy.js +0 -67
- package/core/built/admin/assets/index-DxRNHH0o.css +0 -1
- package/core/built/admin/assets/inline-C22rnPro.js +0 -1
- package/core/built/admin/assets/input-group-CKfrILLP.js +0 -1
- package/core/built/admin/assets/jsx-runtime-CtMZ-PAS.js +0 -1
- package/core/built/admin/assets/kpi-card-DZxQKAIb.js +0 -1
- package/core/built/admin/assets/kpi-card-FidQ5zg3.js +0 -1
- package/core/built/admin/assets/kpi-helpers-C5T8Vaf1.js +0 -1
- package/core/built/admin/assets/kpi-tabs-_VRK6hGV.js +0 -1
- package/core/built/admin/assets/label-Cza7hPgn.js +0 -1
- package/core/built/admin/assets/label-picker-DgC_zLaw.js +0 -1
- package/core/built/admin/assets/layout-BxsXnKFX.js +0 -1
- package/core/built/admin/assets/limit-service-CTurluJQ.js +0 -20
- package/core/built/admin/assets/link-helpers-C3Gqy2cz.js +0 -1
- package/core/built/admin/assets/list-page-BtqCnow_.js +0 -1
- package/core/built/admin/assets/member-custom-fields-9uXFBqSt.js +0 -1
- package/core/built/admin/assets/member-detail-Dq6ZDIxo.js +0 -1
- package/core/built/admin/assets/members-CUWD9jal.js +0 -10
- package/core/built/admin/assets/modals-CIdwHVga.js +0 -31
- package/core/built/admin/assets/moderation-DTt04UDA.js +0 -1
- package/core/built/admin/assets/newsletter-cC8vcc9h.js +0 -1
- package/core/built/admin/assets/note-DZU0IY9e.js +0 -1
- package/core/built/admin/assets/notifications-NoHpBHJ4.js +0 -1
- package/core/built/admin/assets/nql-lang-CvCSOQG3.js +0 -19
- package/core/built/admin/assets/onboarding-route-Deg2hNm8.js +0 -1
- package/core/built/admin/assets/overview-BQPt_9w3.js +0 -1
- package/core/built/admin/assets/page-header-D6bEqPo2.js +0 -1
- package/core/built/admin/assets/post-analytics-header-BPw_B2_D.js +0 -1
- package/core/built/admin/assets/post-analytics-kp2_--XO.js +0 -1
- package/core/built/admin/assets/post-share-modal-Cave3Z4W.js +0 -1
- package/core/built/admin/assets/preferences-C7h3j_uy.js +0 -5
- package/core/built/admin/assets/profile-B2x4q2oB.js +0 -1
- package/core/built/admin/assets/router-provider-5fsX1CYn.js +0 -1
- package/core/built/admin/assets/select-PhPjIBBT.js +0 -1
- package/core/built/admin/assets/settings-D5P9Gwgo.js +0 -6
- package/core/built/admin/assets/settings-zUOzQV5I.js +0 -1
- package/core/built/admin/assets/share-modal-EfpfbuWF.js +0 -1
- package/core/built/admin/assets/social-urls-DgAF08Xc.js +0 -18
- package/core/built/admin/assets/sort-button-WOQYJm2H.js +0 -1
- package/core/built/admin/assets/source-icon-hm8Cj-Gx.js +0 -1
- package/core/built/admin/assets/stats-CnQDb62m.js +0 -1
- package/core/built/admin/assets/stats-view-CCfTJgSe.js +0 -1
- package/core/built/admin/assets/step-1-BGlhY4DS.js +0 -1
- package/core/built/admin/assets/step-2-D3sDEyFH.js +0 -1
- package/core/built/admin/assets/step-3-CvZoUdsc.js +0 -2
- package/core/built/admin/assets/switch-CNjkOmwz.js +0 -1
- package/core/built/admin/assets/table-C5nZYPnW.js +0 -1
- package/core/built/admin/assets/tabs-dtOcExye.js +0 -1
- package/core/built/admin/assets/tags-Dhm0FB_g.js +0 -1
- package/core/built/admin/assets/textarea-CeKw3wmV.js +0 -1
- package/core/built/admin/assets/toggle-group-DfhZIMTW.js +0 -1
- package/core/built/admin/assets/topic-filter-Cxe1Il41.js +0 -1
- package/core/built/admin/assets/typeof-Ca-duI-z.js +0 -1
- package/core/built/admin/assets/use-filter-params-Bub6BqNy.js +0 -1
- package/core/built/admin/assets/use-growth-stats-BJORomv0.js +0 -1
- package/core/built/admin/assets/use-reply-chain-data-B89YjVoo.js +0 -1
- package/core/built/admin/assets/use-simple-pagination-BL2SYXKi.js +0 -1
- package/core/built/admin/assets/virtual-list-DNJOSK9I.js +0 -1
- package/core/built/admin/assets/web-BHUE3YQc.js +0 -1
- package/core/server/adapters/scheduling/scheduling-base.js +0 -5
- package/core/server/adapters/sso/SSOBase.js +0 -5
- package/core/server/adapters/storage/index.js +0 -17
- package/core/server/services/mail/templates/newsletter.html +0 -876
- package/core/server/services/members/exporter/query.js +0 -206
- package/core/server/services/members/importer/index.js +0 -30
- package/core/server/services/members/importer/labels.js +0 -125
- package/core/server/services/members/importer/members-csv-importer.js +0 -502
- /package/core/server/services/members/{importer → import-export/import}/email-template.js +0 -0
- /package/core/server/services/members/{importer/members-csv-importer-stripe-utils.js → import-export/import/stripe-utils.js} +0 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {z} from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolving Ghost's config into the values this service runs on.
|
|
5
|
+
*
|
|
6
|
+
* Config arrives untyped: defaults.json supplies the shipped value, and any
|
|
7
|
+
* higher layer — an operator's config file, an environment variable, argv — can
|
|
8
|
+
* replace it with anything at all. Turning that into something usable is a
|
|
9
|
+
* config concern, so it happens here, at the boundary. The service is handed an
|
|
10
|
+
* answer and never learns where it came from.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// A ceiling in the form the service needs it. A numeric string is accepted
|
|
14
|
+
// because a value supplied through an environment variable can arrive as one.
|
|
15
|
+
const Ceiling = z
|
|
16
|
+
.union([z.number(), z.string().trim().min(1)])
|
|
17
|
+
.transform(Number)
|
|
18
|
+
.pipe(z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER));
|
|
19
|
+
|
|
20
|
+
// defaults.json is the single source of the shipped ceiling: it is the lowest
|
|
21
|
+
// config layer and the file an operator actually reads and edits, so it is read
|
|
22
|
+
// rather than restated. Parsed eagerly because a missing or malformed value
|
|
23
|
+
// there is our bug rather than an operator's, and should fail loudly at boot
|
|
24
|
+
// instead of quietly becoming the fallback for everything below.
|
|
25
|
+
const shippedCeiling: number = Ceiling.parse(
|
|
26
|
+
require('../../../shared/config/defaults.json').members.customFields.maxDefinitions
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
// A higher layer can override the shipped value with anything. Something
|
|
30
|
+
// unreadable falls back to the shipped ceiling rather than throwing, because an
|
|
31
|
+
// operator can correct this live and a bad value must not take the feature down
|
|
32
|
+
// with it — and rather than meaning "no ceiling", because a typo must not
|
|
33
|
+
// silently remove the safeguard.
|
|
34
|
+
const MaxDefinitions = Ceiling.catch(shippedCeiling);
|
|
35
|
+
|
|
36
|
+
export function resolveMaxDefinitions(configured: unknown): number {
|
|
37
|
+
return MaxDefinitions.parse(configured);
|
|
38
|
+
}
|
|
@@ -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
|
-
|
|
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
|
|
118
|
+
const requestedCount = Array.isArray(input) ? input.length : 0;
|
|
119
|
+
const parsed = AddFieldsInput.safeParse(input);
|
|
79
120
|
if (!parsed.success) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
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
|
-
|
|
102
|
-
|
|
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(
|
|
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 =
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
94
|
-
if (!
|
|
95
|
-
|
|
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
|
-
|
|
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
|
-
|
|
101
|
-
const key = await this.mintKey(base);
|
|
158
|
+
let created: CustomField[];
|
|
102
159
|
try {
|
|
103
|
-
await this.knex
|
|
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
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
20
|
-
exports.values = new values_service_1.CustomFieldValuesService({
|
|
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
|
-
|
|
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
|
|
35
|
-
values = new CustomFieldValuesService({
|
|
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
|
}
|