ghost 6.61.0 → 6.63.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.
- 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-jobs-0.0.0.tgz +0 -0
- package/components/tryghost-adapter-base-redirects-0.0.0.tgz +0 -0
- package/components/tryghost-adapter-base-route-settings-0.0.0.tgz +0 -0
- package/components/tryghost-adapter-base-scheduling-0.2.3.tgz +0 -0
- package/components/tryghost-adapter-base-sso-0.1.3.tgz +0 -0
- package/components/tryghost-admin-api-schema-0.0.0.tgz +0 -0
- package/components/tryghost-checkout-0.0.0.tgz +0 -0
- package/components/tryghost-i18n-0.0.0.tgz +0 -0
- package/components/tryghost-kg-card-factory-5.2.4.tgz +0 -0
- package/components/tryghost-kg-clean-basic-html-4.3.4.tgz +0 -0
- package/components/tryghost-kg-converters-1.2.5.tgz +0 -0
- package/components/tryghost-kg-default-cards-10.3.5.tgz +0 -0
- package/components/tryghost-kg-default-nodes-2.2.1.tgz +0 -0
- package/components/tryghost-kg-default-transforms-1.3.4.tgz +0 -0
- package/components/tryghost-kg-html-to-lexical-1.4.0.tgz +0 -0
- package/components/tryghost-kg-lexical-html-renderer-1.5.0.tgz +0 -0
- package/components/tryghost-kg-markdown-html-renderer-7.2.4.tgz +0 -0
- package/components/tryghost-kg-utils-1.1.4.tgz +0 -0
- package/components/tryghost-limit-service-0.0.0.tgz +0 -0
- package/components/tryghost-metafield-types-0.0.0.tgz +0 -0
- package/components/tryghost-nql-string-0.0.0.tgz +0 -0
- package/components/tryghost-parse-email-address-0.0.0.tgz +0 -0
- package/content/settings/README.md +1 -1
- package/content/themes/casper/.gitignore +28 -0
- package/content/themes/casper/assets/built/casper.js +1 -1
- package/content/themes/casper/assets/built/screen.css +1 -1
- package/content/themes/casper/assets/css/screen.css +2 -1
- package/content/themes/casper/assets/js/lightbox.js +63 -46
- package/content/themes/casper/package.json +6 -6
- package/content/themes/casper/page.hbs +1 -0
- package/content/themes/casper/post.hbs +1 -0
- package/content/themes/source/.gitignore +28 -0
- package/content/themes/source/assets/built/screen.css +1 -1
- package/content/themes/source/assets/built/source.js +1 -1
- package/content/themes/source/assets/css/screen.css +2 -1
- package/content/themes/source/assets/js/lightbox.js +62 -45
- package/content/themes/source/assets/js/main.js +2 -2
- package/content/themes/source/package.json +4 -4
- package/content/themes/source/partials/feature-image.hbs +1 -0
- package/content/themes/source/partials/post-card.hbs +1 -1
- package/core/boot.js +61 -27
- package/core/built/admin/assets/Combination-B0l4MKVh.js +41 -0
- package/core/built/admin/assets/InterVariable-DiVDrmQJ.woff2 +0 -0
- package/core/built/admin/assets/InterVariable-Italic-FCBEiFp6.woff2 +0 -0
- package/core/built/admin/assets/{about-Bve7IJS2.js → about-C1C3s5-g.js} +1 -1
- package/core/built/admin/assets/accordion-CxGsDKwa.js +1 -0
- package/core/built/admin/assets/account-migration-CbUhW6gk.js +1 -0
- package/core/built/admin/assets/activitypub/{account-migration-C-aej4x_.js → account-migration-KA6Jjf9L.js} +16 -16
- package/core/built/admin/assets/activitypub/activitypub.js +9 -9
- package/core/built/admin/assets/activitypub/{avatar-DLPL3Abs.js → avatar-DVJF5bxP.js} +2 -2
- package/core/built/admin/assets/activitypub/{avatar-flipboard-C9iDY-QV.js → avatar-flipboard-csQaNeYK.js} +2 -2
- package/core/built/admin/assets/activitypub/{bluesky-sharing-BXkA5mqV.js → bluesky-sharing-C7nTDMvw.js} +37 -37
- package/core/built/admin/assets/activitypub/{content-formatters-CHcyMFk4.js → content-formatters-CLcjaBwV.js} +4 -4
- package/core/built/admin/assets/activitypub/{copy-DIAOQoCZ.js → copy-DcOh8kXw.js} +2 -2
- package/core/built/admin/assets/activitypub/{domain-B3ihkPde.js → domain-fZ-X8i4-.js} +121 -121
- package/core/built/admin/assets/activitypub/{edit-profile-DsZM3rTX.js → edit-profile-BqMLQV88.js} +40 -40
- package/core/built/admin/assets/activitypub/{explore-BodNrZgR.js → explore-B4yPoP8E.js} +27 -27
- package/core/built/admin/assets/activitypub/{feed-PwH1iJ84.js → feed-BRC_mvAe.js} +51 -51
- package/core/built/admin/assets/activitypub/{field-D06-kmOJ.js → field-COacNgOt.js} +3 -3
- package/core/built/admin/assets/activitypub/{hash-Bm3WxYN4.js → hash-C7u-irQQ.js} +2 -2
- package/core/built/admin/assets/activitypub/inbox-BW-yvUoN.js +1284 -0
- package/core/built/admin/assets/activitypub/{inbox-d8oJRTf4.js → inbox-CbTTdDQ3.js} +2 -2
- package/core/built/admin/assets/activitypub/label-kPMmYI7l.js +16 -0
- package/core/built/admin/assets/activitypub/layout-BL4_jtuV.js +1 -0
- package/core/built/admin/assets/activitypub/{moderation-D1Yapsod.js → moderation-CuidJzc0.js} +91 -91
- package/core/built/admin/assets/activitypub/{note-C5zjqq5C.js → note-CAMRRSqD.js} +24 -24
- package/core/built/admin/assets/activitypub/{notifications-B9p8eCHD.js → notifications-BmNlXGgp.js} +51 -51
- package/core/built/admin/assets/activitypub/{onboarding-CNelD_uF.js → onboarding-Bqg7fh_L.js} +2 -2
- package/core/built/admin/assets/activitypub/{onboarding-mat5Hrl0.js → onboarding-Dmun9jr_.js} +2 -2
- package/core/built/admin/assets/activitypub/{preferences-BCqUdC3v.js → preferences--oH19wf9.js} +1519 -1519
- package/core/built/admin/assets/activitypub/{profile-BRCWoFem.js → profile-DH_tQx2D.js} +185 -185
- package/core/built/admin/assets/activitypub/{reply-DYK5vVqb.js → reply-Cap9PxSH.js} +2 -2
- package/core/built/admin/assets/activitypub/{routes-BSXT992i.js → routes-DdyY9uFF.js} +744 -735
- package/core/built/admin/assets/activitypub/{separator-BbQqTm-p.js → separator-CAxxu8VV.js} +3 -3
- package/core/built/admin/assets/activitypub/settings-B6EEcVkW.js +315 -0
- package/core/built/admin/assets/activitypub/{site-DHoMupSB.js → site-BYps63oh.js} +2 -2
- package/core/built/admin/assets/activitypub/{step-1-B-YfsVeO.js → step-1-CKjR2KAm.js} +6 -6
- package/core/built/admin/assets/activitypub/{step-2-DtPQzlTH.js → step-2-B6PKfOUQ.js} +6 -6
- package/core/built/admin/assets/activitypub/{step-3-D6BFMXL_.js → step-3-C-qBbcUo.js} +11 -11
- package/core/built/admin/assets/activitypub/{tabs-GpNecvfB.js → tabs-DLhZtVde.js} +306 -306
- package/core/built/admin/assets/activitypub/{topic-filter-BPDeQQys.js → topic-filter-DsUOuu0k.js} +3 -3
- package/core/built/admin/assets/activitypub/{use-activity-pub-queries-BoVOxXkV.js → use-activity-pub-queries-CX9snsLs.js} +3 -3
- package/core/built/admin/assets/activitypub/{use-navigate-with-base-path-Bmk5h4lW.js → use-navigate-with-base-path-DsJTGDSS.js} +988 -959
- package/core/built/admin/assets/activitypub/{use-reply-chain-data-wVBX7swM.js → use-reply-chain-data-AJi9zkzm.js} +30 -30
- package/core/built/admin/assets/activitypub/{users-CoX6FT_K.js → users-inweyKL5.js} +192 -164
- package/core/built/admin/assets/activitypub/{x-DbQZOLiI.js → x-DR8AT3-4.js} +2 -2
- package/core/built/admin/assets/add-integration-modal-BT_onDJy.js +1 -0
- package/core/built/admin/assets/add-newsletter-modal-Cx1kQpiY.js +1 -0
- package/core/built/admin/assets/add-recommendation-modal-B5nvAhvr.js +1 -0
- package/core/built/admin/assets/analytics-DjoJWPI7.js +1 -0
- package/core/built/admin/assets/{announcement-bar-modal-DM3cYOpo.js → announcement-bar-modal-Dwd0EGFP.js} +1 -1
- package/core/built/admin/assets/{api-keys-3-0vJC3Y.js → api-keys-BGPzLHzA.js} +1 -1
- package/core/built/admin/assets/{api-keys-BLm2NDBh.js → api-keys-BoeacM-s.js} +1 -1
- package/core/built/admin/assets/{audience-9mwU5TW6.js → audience-ceV3XVHq.js} +1 -1
- package/core/built/admin/assets/automated-emails-BLesE41L.js +1 -0
- package/core/built/admin/assets/{automations-sAcJ1334.js → automations-BN4u3GR8.js} +1 -1
- package/core/built/admin/assets/automations-C-ZTUwW3.js +1 -0
- package/core/built/admin/assets/bluesky-sharing-vzh-oPXL.js +1 -0
- package/core/built/admin/assets/{breadcrumb-DLiqCyFc.js → breadcrumb-CaLk2b7D.js} +1 -1
- package/core/built/admin/assets/{button-B2jcu6bk.js → button-Dvcv1QSR.js} +1 -1
- package/core/built/admin/assets/calendar-Bl9RLcZX.js +1 -0
- package/core/built/admin/assets/{chart-helpers-Fo3bUL3K.js → chart-helpers-FHywU0pj.js} +1 -1
- package/core/built/admin/assets/{checkbox-BfKktMph.js → checkbox-BtayFtjg.js} +1 -1
- package/core/built/admin/assets/chunk.254.035b85ec456ebbd7336b.js +440 -0
- package/core/built/admin/assets/{chunk.524.60ca4692d91c6022042f.js → chunk.524.e6975095fccdf22d90f8.js} +5 -5
- package/core/built/admin/assets/{chunk.582.525c7617c2d9e94c940a.js → chunk.582.98b485f9b2802f540897.js} +6 -6
- package/core/built/admin/assets/{chunk.492.f24d855d7e3f3aad261c.js → chunk.901.4ccc639821020230bcca.js} +9949 -7921
- package/core/built/admin/assets/{code-editor-view-DCQJxsfU.js → code-editor-view-C3ZfoF1X.js} +1 -1
- package/core/built/admin/assets/{code-editor-view-CYVu4EW6.js → code-editor-view-CAyRWy1s.js} +1 -1
- package/core/built/admin/assets/color-picker-G4kGR7rp.js +3 -0
- package/core/built/admin/assets/color-picker-field-DQFVTNnT.js +1 -0
- package/core/built/admin/assets/combobox-BnYzgEB4.js +1 -0
- package/core/built/admin/assets/command-C_vyTikI.js +1 -0
- package/core/built/admin/assets/comments-aBrxfnZf.js +1 -0
- package/core/built/admin/assets/{config-DOMQDZGZ.js → config-kSHvwdTG.js} +1 -1
- package/core/built/admin/assets/confirmation-context-DwXX_Y5h.js +1 -0
- package/core/built/admin/assets/{content-api-modal-CyQEC3fr.js → content-api-modal-D2G0nJP6.js} +1 -1
- package/core/built/admin/assets/context-menu-DOVB6yaz.js +1 -0
- package/core/built/admin/assets/{copy-field-B3M1s78L.js → copy-field-DCJwh9qf.js} +1 -1
- package/core/built/admin/assets/create-ghost-browse-value-source-5C53jJaX.js +1 -0
- package/core/built/admin/assets/custom-field-type-option-BAuW0F-E.js +1 -0
- package/core/built/admin/assets/custom-integration-modal-AbJGVyKc.js +1 -0
- package/core/built/admin/assets/{date-range-select-DD2RKkgB.js → date-range-select-BY3K-ukj.js} +1 -1
- package/core/built/admin/assets/design-and-theme-modal-BvEy4_pC.js +2 -0
- package/core/built/admin/assets/dirty-confirm-dialog-DVPuGz3U.js +1 -0
- package/core/built/admin/assets/{disabled-sources-indicator-Cvy47bE2.js → disabled-sources-indicator-Cy7Zp4c8.js} +1 -1
- package/core/built/admin/assets/dist-Dcsslyo72.js +1 -0
- package/core/built/admin/assets/domain-Dl61QGuk.js +1 -0
- package/core/built/admin/assets/dropzone-Dtz5Rmjt.js +5 -0
- package/core/built/admin/assets/edit-profile-DNJSMDwm.js +1 -0
- package/core/built/admin/assets/editor-DbAen9zf.js +8 -0
- package/core/built/admin/assets/editor-screen-BFWThIai.js +26 -0
- package/core/built/admin/assets/email-sending-status-context-CWDOD1FN.js +1 -0
- package/core/built/admin/assets/email-sending-status-provider-DCPkDICt.js +1 -0
- package/core/built/admin/assets/emails-vFMk4yYl.js +1 -0
- package/core/built/admin/assets/{embed-signup-form-modal-DeBMLxlL.js → embed-signup-form-modal-DGqsORxS.js} +1 -1
- package/core/built/admin/assets/en-B1jX3WMs.js +1 -0
- package/core/built/admin/assets/explore-nnRr1Fja.js +1 -0
- package/core/built/admin/assets/feature-flag-overrides-context-DQjdJFAk.js +1 -0
- package/core/built/admin/assets/feature-image-placeholder-BrIHmPs8.js +1 -0
- package/core/built/admin/assets/feed-BNUcNu-W.js +1 -0
- package/core/built/admin/assets/feedback-DJ4Zb9rZ.js +1 -0
- package/core/built/admin/assets/{field-Cj8lmDOo.js → field-C93_Mg6j.js} +1 -1
- package/core/built/admin/assets/filter-bar-Da1qxybu.js +1 -0
- package/core/built/admin/assets/filter-sources-CGJb4C5r.js +1 -0
- package/core/built/admin/assets/filters--kKP-EC8.js +1 -0
- package/core/built/admin/assets/filters-DxA_a4FP.js +1 -0
- package/core/built/admin/assets/first-promoter-modal-BYDtrz2S.js +1 -0
- package/core/built/admin/assets/focus-koenig-editor-on-bottom-click-BbSnGOtK.js +1 -0
- package/core/built/admin/assets/{ghost-c8807202a07c20030acb719ee0114815.css → ghost-8fd5bb799356bbefb254210ba7e1236d.css} +1 -1
- package/core/built/admin/assets/{ghost-3894eedcc6968f658578c0abfe942aab.js → ghost-b4e8a403baba61905cd7560e30b9732d.js} +63 -44
- package/core/built/admin/assets/{ghost-dark-fbf1e384aaef3ae315e540f0eb7373a7.css → ghost-dark-cac0cb2d5bb78e9bd14d8d1c2768ed45.css} +1 -1
- package/core/built/admin/assets/gift-link-modal-B7u5bD4e.js +1 -0
- package/core/built/admin/assets/{gift-links-DLDS7ljO.js → gift-links-CfLSnst2.js} +1 -1
- package/core/built/admin/assets/{growth-D6mhcx3q.js → growth-Cd1uO9d2.js} +1 -1
- package/core/built/admin/assets/growth-aG32xfdE.js +3 -0
- package/core/built/admin/assets/history-modal-Df8lW8h-.js +1 -0
- package/core/built/admin/assets/hooks-Cy_-oZ4o.js +2 -0
- package/core/built/admin/assets/{html-field-C8qVGGC1.js → html-field-DA4eU7eq.js} +1 -1
- package/core/built/admin/assets/{images-h8VZEQ9k.js → images-KXmyO3Ss.js} +1 -1
- package/core/built/admin/assets/{inbox-BYd1M06Q.js → inbox-gJHG7_xd.js} +14 -14
- package/core/built/admin/assets/index-BHHXtR5C.css +2 -0
- package/core/built/admin/assets/index-CgMCwsxN.js +4 -0
- package/core/built/admin/assets/index.es-CY6-yuw9.js +1 -0
- package/core/built/admin/assets/{input-group-D8sCXRF5.js → input-group-BE8rM2y1.js} +1 -1
- package/core/built/admin/assets/{integrations-Bjbu86Z0.js → integrations-B4kuRBFY.js} +1 -1
- package/core/built/admin/assets/invite-user-modal-B4CuNcx4.js +1 -0
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +14 -14
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +3 -3
- package/core/built/admin/assets/koenig-lexical/style.css +1 -1
- package/core/built/admin/assets/{koenig-lexical-_-IsVI7E.js → koenig-lexical-CGXb0F8o.js} +3 -3
- package/core/built/admin/assets/koenig-loader-BeM5rFPX.js +2 -0
- package/core/built/admin/assets/{kpi-card-DZQk_0W8.js → kpi-card-B21aToe8.js} +1 -1
- package/core/built/admin/assets/{kpi-card-DW_2y3ug.js → kpi-card-pz6Wv4Ae.js} +1 -1
- package/core/built/admin/assets/{kpi-helpers-PueTKuA6.js → kpi-helpers-Dq5jelmP.js} +1 -1
- package/core/built/admin/assets/{kpi-tabs-CojDs-Qz.js → kpi-tabs-DEZHpKUt.js} +1 -1
- package/core/built/admin/assets/{label-DjbT91GM.js → label-OLCzze8N.js} +1 -1
- package/core/built/admin/assets/label-picker-BigOvMPV.js +1 -0
- package/core/built/admin/assets/{labels-CH3Kn8RV.js → labels-Dr0SgUQD.js} +1 -1
- package/core/built/admin/assets/layout-DUTL8SH5.js +1 -0
- package/core/built/admin/assets/lib-7QPTI0FS.js +1 -0
- package/core/built/admin/assets/link-helpers-BRAg9SCj.js +1 -0
- package/core/built/admin/assets/links-BDHPTlyj.js +1 -0
- package/core/built/admin/assets/member-custom-fields-TRTHCAz9.js +1 -0
- package/core/built/admin/assets/member-detail-b6dRwc3C.js +1 -0
- package/core/built/admin/assets/members-Czt5lJSc.js +4 -0
- package/core/built/admin/assets/moderation-ZWefD9uH.js +1 -0
- package/core/built/admin/assets/{multi-select-combobox-B6LiRzDO.js → multi-select-combobox-CT13MTo-.js} +1 -1
- package/core/built/admin/assets/navigation-modal-C8FEhBK5.js +1 -0
- package/core/built/admin/assets/newsletter-5VYW4RyJ.js +1 -0
- package/core/built/admin/assets/newsletter-detail-modal-ChjC0ZqH.js +1 -0
- package/core/built/admin/assets/{newsletter-emails-CFeLlt4z.js → newsletter-emails-DZH6I-PY.js} +1 -1
- package/core/built/admin/assets/newsletters-DOC9KBcX.js +1 -0
- package/core/built/admin/assets/newsletters-Rq1M3GD-.js +1 -0
- package/core/built/admin/assets/note-DIV-Pojr.js +1 -0
- package/core/built/admin/assets/notifications-s20-H49V.js +1 -0
- package/core/built/admin/assets/nql-BLpCn7qX.js +21 -0
- package/core/built/admin/assets/{offers-9J1CSiLy.js → offers--e2nGvkW.js} +1 -1
- package/core/built/admin/assets/offers-container-modal-BmG5M5Tv.js +1 -0
- package/core/built/admin/assets/onboarding-CUuy9unR.js +1 -0
- package/core/built/admin/assets/{onboarding-B4YhlZ0i.js → onboarding-pXyct0Jm.js} +1 -1
- package/core/built/admin/assets/{onboarding-route-Dmc4IPm3.js → onboarding-route-CK4pcXoR.js} +1 -1
- package/core/built/admin/assets/overview-CaPsciMs.js +1 -0
- package/core/built/admin/assets/overview-a8GfcsKX.js +1 -0
- package/core/built/admin/assets/{page-header-BR_nbIPp.js → page-header-DM_KyoFI.js} +1 -1
- package/core/built/admin/assets/pagemenu-vgZVUVKR.js +1 -0
- package/core/built/admin/assets/pages-CjDY2CBI.js +1 -0
- package/core/built/admin/assets/pages-route-BzKZYdV5.js +1 -0
- package/core/built/admin/assets/papaparse.min-cQwnxGCG.js +10 -0
- package/core/built/admin/assets/{pintura-modal-BR9hEvS8.js → pintura-modal-DExCF1ii.js} +1 -1
- package/core/built/admin/assets/portal-frame-Cqanidq4.js +1 -0
- package/core/built/admin/assets/portal-modal-oinBgA27.js +1 -0
- package/core/built/admin/assets/post-analytics-C5oJ1dWV.js +1 -0
- package/core/built/admin/assets/post-analytics-header-OATo3X_r.js +1 -0
- package/core/built/admin/assets/post-analytics-provider-tOpIVGT5.js +1 -0
- package/core/built/admin/assets/post-helpers-C88JBa4D.js +1 -0
- package/core/built/admin/assets/post-share-modal-DvhC1XgV.js +1 -0
- package/core/built/admin/assets/post-utils-DKF4mfuo.js +1 -0
- package/core/built/admin/assets/posts-list-screen-o4ZjbgS3.js +2 -0
- package/core/built/admin/assets/posts-pCPrhQeR.js +1 -0
- package/core/built/admin/assets/posts-route-9F--xdIb.js +1 -0
- package/core/built/admin/assets/preferences--vNubsqD.js +5 -0
- package/core/built/admin/assets/{preview-modal-C-BL-F9k.js → preview-modal-YYbzlYdO.js} +1 -1
- package/core/built/admin/assets/profile-BllKV3dR.js +1 -0
- package/core/built/admin/assets/{radio-group-Bgz60i81.js → radio-group-DL8GI8rt.js} +1 -1
- package/core/built/admin/assets/{referrers-CnhCMWCs.js → referrers-D8oHPmDB.js} +1 -1
- package/core/built/admin/assets/router-provider-BSvTXgZY.js +1 -0
- package/core/built/admin/assets/schemas-BoYdB6YU.js +64 -0
- package/core/built/admin/assets/select-v5YbA3Mw.js +1 -0
- package/core/built/admin/assets/settings-PEsZupw1.js +4 -0
- package/core/built/admin/assets/{settings-breadcrumbs-CyP_9zTo.js → settings-breadcrumbs-NwPnCTC7.js} +1 -1
- package/core/built/admin/assets/settings-u_s1CRkr.js +1 -0
- package/core/built/admin/assets/share-modal-SskCmVGL.js +1 -0
- package/core/built/admin/assets/slack-modal-XxSJiYgo.js +1 -0
- package/core/built/admin/assets/{sort-button-Cc-Z96sA.js → sort-button-Cx1KtINE.js} +1 -1
- package/core/built/admin/assets/{source-icon-v6PUnmyM.js → source-icon-Bdp9Im_5.js} +1 -1
- package/core/built/admin/assets/stats-D9PDLS3x.js +1 -0
- package/core/built/admin/assets/stats-view-W2g6Jsd-.js +1 -0
- package/core/built/admin/assets/{step-1-DSRp2vBj.js → step-1-BQYzvBzr.js} +1 -1
- package/core/built/admin/assets/{step-2-COqv2g0u.js → step-2-xES32xJV.js} +1 -1
- package/core/built/admin/assets/{step-3-6W1NMVQc.js → step-3-DA_XK5kW.js} +2 -2
- package/core/built/admin/assets/{stripe-button-D2K42Uty.js → stripe-button-CVsLHelt.js} +1 -1
- package/core/built/admin/assets/stripe-connect-modal-ClmLOMGS.js +1 -0
- package/core/built/admin/assets/style-Wd9nQubI.css +1 -0
- package/core/built/admin/assets/{table-B2pIKjVh.js → table-BenqVupI.js} +1 -1
- package/core/built/admin/assets/{tabs-SaW_UT-Q.js → tabs-eBBlxPp8.js} +1 -1
- package/core/built/admin/assets/tag-detail-Ce8Se2qd.js +2 -0
- package/core/built/admin/assets/tag-picker-C3UR-uJ-.js +1 -0
- package/core/built/admin/assets/tags-B-kJxCs7.js +1 -0
- package/core/built/admin/assets/{tags-zTFiKRpb.js → tags-ChfdwJyS.js} +1 -1
- package/core/built/admin/assets/testimonials-modal-9Q6fT06V.js +1 -0
- package/core/built/admin/assets/tier-detail-modal-CDyYdqK7.js +1 -0
- package/core/built/admin/assets/tiers-BCpIOGQr.js +1 -0
- package/core/built/admin/assets/{toggle-group-EM9WpESu.js → toggle-group-CviIkyok.js} +1 -1
- package/core/built/admin/assets/{topic-filter-DY4gNHn2.js → topic-filter-I2jCBGxm.js} +1 -1
- package/core/built/admin/assets/{transistor-modal-CpyPG-MH.js → transistor-modal-p53u3W8y.js} +1 -1
- package/core/built/admin/assets/{trend-badge-ByMkuAuH.js → trend-badge-CideVzQd.js} +1 -1
- package/core/built/admin/assets/{unsplash-modal-Cv2Q6GtY.js → unsplash-modal-DY8UcKcE.js} +1 -1
- package/core/built/admin/assets/{use-analytics-data-CHvjMYuT.js → use-analytics-data-DAIGcr2f.js} +1 -1
- package/core/built/admin/assets/use-check-theme-limit-error-B8bjF-gO.css +1 -0
- package/core/built/admin/assets/{use-check-theme-limit-error-Qv4SVXqw.js → use-check-theme-limit-error-DRUjK4YM.js} +1 -1
- package/core/built/admin/assets/use-filter-params-JkZhIS8L.js +1 -0
- package/core/built/admin/assets/{use-filterable-api-CzXcpUh8.js → use-filterable-api-BVEVMrhZ.js} +1 -1
- package/core/built/admin/assets/use-growth-stats-937CUj6j.js +1 -0
- package/core/built/admin/assets/use-host-limits-D0Dv3tKA.js +1 -0
- package/core/built/admin/assets/{use-limiter-cwJvsNBo.js → use-limiter-CohWfzvK.js} +1 -1
- package/core/built/admin/assets/{use-navigate-with-base-path-BHPA228f.js → use-navigate-with-base-path-BkxcPj5l.js} +1 -1
- package/core/built/admin/assets/use-pintura-config-xgytjU35.js +1 -0
- package/core/built/admin/assets/{use-pintura-editor-BDBXWUYq.js → use-pintura-editor-CaV1mbfO.js} +1 -1
- package/core/built/admin/assets/{use-post-referrers-DbY-mzci.js → use-post-referrers-BCQI7fDg.js} +1 -1
- package/core/built/admin/assets/use-reply-chain-data-CYRhoaxY.js +1 -0
- package/core/built/admin/assets/{use-router-history-entry-Bg6BhBQQ.js → use-router-history-entry-NOKcd--l.js} +1 -1
- package/core/built/admin/assets/{use-setting-group-VM7FQwMY.js → use-setting-group-bLKMwoBT.js} +1 -1
- package/core/built/admin/assets/use-settings-navigation-B-qrriKD.js +1 -0
- package/core/built/admin/assets/{use-simple-pagination-Dum6Irbs.js → use-simple-pagination-CNPd6Rf6.js} +1 -1
- package/core/built/admin/assets/use-staff-users-pRIMIyiW.js +1 -0
- package/core/built/admin/assets/use-tinybird-query-CmdbCS9X.js +1 -0
- package/core/built/admin/assets/use-tinybird-token-KLne235s.js +1 -0
- package/core/built/admin/assets/{use-unsaved-changes-guard-BAy0RANR.js → use-unsaved-changes-guard-7-ug0JdH.js} +1 -1
- package/core/built/admin/assets/use-upgrade-route-Dm_qqKr0.js +1 -0
- package/core/built/admin/assets/{use-url-input-Df5QzNa7.js → use-url-input-fvp-TpPx.js} +1 -1
- package/core/built/admin/assets/user-detail-modal-D3KlGYv-.js +1 -0
- package/core/built/admin/assets/users-DVNWrqg9.js +1 -0
- package/core/built/admin/assets/{vendor-5753232c7d6ffb3c44e1eb0a02133953.js → vendor-2dd263291938d407802568d3357ec69e.js} +19 -17
- package/core/built/admin/assets/{virtual-list-DEx8WgiO.js → virtual-list-CZ3RK2Yf.js} +1 -1
- package/core/built/admin/assets/web-ClEJsCAj.js +2 -0
- package/core/built/admin/assets/web-X9rdnrtq.js +1 -0
- package/core/built/admin/assets/{zapier-modal-Cw3BTBUu.js → zapier-modal-DfVhV17j.js} +1 -1
- package/core/built/admin/index.html +23 -27
- package/core/frontend/apps/private-blogging/lib/views/private.hbs +44 -43
- package/core/frontend/helpers/cancel_link.js +2 -2
- package/core/frontend/helpers/pagination.js +2 -2
- package/core/frontend/helpers/social_accounts.js +2 -2
- package/core/frontend/public/ghost.css +1 -1
- package/core/frontend/public/ghost.min.css +1 -1
- package/core/frontend/services/sitemap/user-map-generator.js +9 -2
- package/core/server/adapters/jobs/InMemoryJobsBackend.js +74 -29
- package/core/server/api/endpoints/authentication.js +1 -2
- package/core/server/api/endpoints/authors-public.js +15 -5
- package/core/server/api/endpoints/emails.js +13 -0
- package/core/server/api/endpoints/index.js +10 -2
- package/core/server/api/endpoints/mail.js +1 -1
- package/core/server/api/endpoints/member-metafields-members.js +23 -0
- package/core/server/api/endpoints/member-metafields.js +102 -0
- package/core/server/api/endpoints/members-account.js +42 -0
- package/core/server/api/endpoints/members.js +4 -4
- package/core/server/api/endpoints/newsletters-public.js +10 -1
- package/core/server/api/endpoints/pages-public.js +15 -5
- package/core/server/api/endpoints/posts-public.js +15 -5
- package/core/server/api/endpoints/posts.js +3 -3
- package/core/server/api/endpoints/tags-public.js +22 -3
- package/core/server/api/endpoints/tiers-checkout-config.js +2 -2
- package/core/server/api/endpoints/tiers-public.js +38 -1
- package/core/server/api/endpoints/users.js +2 -2
- package/core/server/api/endpoints/utils/api-filter-utils.js +170 -3
- package/core/server/api/endpoints/utils/public-cache-keys.js +3 -0
- package/core/server/api/endpoints/utils/serializers/input/authors.js +2 -0
- package/core/server/api/endpoints/utils/serializers/input/members.js +20 -9
- package/core/server/api/endpoints/utils/serializers/input/pages.js +2 -0
- package/core/server/api/endpoints/utils/serializers/input/posts.js +2 -0
- package/core/server/api/endpoints/utils/serializers/input/tags.js +2 -0
- package/core/server/api/endpoints/utils/serializers/output/emails.js +7 -0
- package/core/server/api/endpoints/utils/serializers/output/index.js +6 -2
- package/core/server/api/endpoints/utils/serializers/output/member-metafields.js +18 -0
- package/core/server/api/endpoints/utils/serializers/output/members-account.js +19 -0
- package/core/server/api/endpoints/utils/serializers/output/members-csv-transform.js +1 -1
- package/core/server/api/endpoints/utils/serializers/output/members.js +2 -4
- package/core/server/api/endpoints/utils/serializers/output/tiers.js +10 -2
- package/core/server/api/endpoints/utils/serializers/output/users.js +0 -10
- package/core/server/api/endpoints/utils/validators/input/invitations.js +0 -5
- package/core/server/api/endpoints/utils/validators/input/pages.js +1 -1
- package/core/server/api/endpoints/utils/validators/input/posts.js +1 -1
- package/core/server/data/exporter/table-lists.js +3 -3
- package/core/server/data/importer/importers/data/data-importer.js +3 -3
- package/core/server/data/migrations/utils/migrations.js +1 -1
- package/core/server/data/migrations/versions/6.53/2026-07-15-20-22-53-add-members-custom-field-values.js +1 -1
- package/core/server/data/migrations/versions/6.57/2026-08-04-18-00-00-store-custom-field-values-as-leaf-rows.js +10 -1
- package/core/server/data/migrations/versions/6.63/2026-09-03-10-07-03-rename-custom-field-tables-to-metafields.js +169 -0
- package/core/server/data/migrations/versions/6.63/2026-09-03-11-03-31-remove-member-custom-field-read-permissions.js +30 -0
- package/core/server/data/schema/fixtures/fixtures.json +0 -11
- package/core/server/data/schema/lib/default-index-name.js +8 -11
- package/core/server/data/schema/schema.js +18 -18
- package/core/server/data/seeders/README.md +48 -0
- package/core/server/data/tinybird/pipes/mv_hits.pipe +86 -50
- package/core/server/lib/db-types/count.js +16 -0
- package/core/server/lib/image/image-size.js +24 -0
- package/core/server/lib/package-json/parse.js +2 -2
- package/core/server/lib/validate-password.js +81 -120
- package/core/server/models/action.js +4 -4
- package/core/server/models/base/plugins/filtered-collection.js +16 -0
- package/core/server/models/member.js +10 -21
- package/core/server/models/post.js +2 -2
- package/core/server/models/relations/authors.js +2 -16
- package/core/server/models/user.js +10 -3
- package/core/server/services/auth/passwordreset.js +13 -0
- package/core/server/services/automations/automations-api.js +1 -1
- package/core/server/services/automations/database-automations-repository.js +11 -4
- package/core/server/services/comments/comments-controller.js +30 -9
- package/core/server/services/comments/comments-service.js +55 -5
- package/core/server/services/content-import/csv/formula.js +12 -0
- package/core/server/services/content-import/csv/index.js +6 -1
- package/core/server/services/content-import/csv/parse.js +27 -6
- package/core/server/services/content-import/csv/serialize.js +13 -0
- package/core/server/services/content-import/import/completion-email.js +164 -0
- package/core/server/services/content-import/import/errors-file.js +36 -0
- package/core/server/services/content-import/import/importer.js +160 -53
- package/core/server/services/content-import/import/local-media-url.js +63 -0
- package/core/server/services/content-import/import/media.js +254 -0
- package/core/server/services/content-import/import/post-data.js +0 -1
- package/core/server/services/content-import/import/post-link.js +11 -0
- package/core/server/services/content-import/import/post-repository.js +25 -3
- package/core/server/services/content-import/import/reader.js +9 -2
- package/core/server/services/content-import/import/report.js +34 -0
- package/core/server/services/content-import/import/row.js +0 -2
- package/core/server/services/content-import/import/schema.js +1 -0
- package/core/server/services/content-import/import/staged-file.js +29 -0
- package/core/server/services/content-import/import/store.js +25 -2
- package/core/server/services/content-import/index.js +52 -6
- package/core/server/services/content-import/jobs/content-csv-import-job.js +20 -0
- package/core/server/services/custom-redirects/redirect-config-parser.js +2 -2
- package/core/server/services/custom-redirects/redirects-service.js +1 -1
- package/core/server/services/custom-redirects/validation.js +1 -1
- package/core/server/services/email-address/email-address-parser.js +30 -38
- package/core/server/services/email-address/email-address-service.js +37 -4
- package/core/server/services/email-analytics/index.js +2 -7
- package/core/server/services/email-service/CONTEXT.md +37 -0
- package/core/server/services/email-service/README.md +42 -0
- package/core/server/services/email-service/batch-sending-service.js +2 -4
- package/core/server/services/email-service/email-controller.js +18 -2
- package/core/server/services/email-service/email-renderer.js +14 -4
- package/core/server/services/email-service/email-service-wrapper.js +3 -9
- package/core/server/services/email-service/mailgun-email-provider.js +20 -9
- package/core/server/services/email-service/sending-status-schema.js +21 -0
- package/core/server/services/email-service/sending-status-serializers.js +33 -0
- package/core/server/services/email-service/sending-status-service.js +47 -0
- package/core/server/services/email-service/sending-status.js +111 -0
- package/core/server/services/email-suppression-list/email-suppression-list.js +0 -33
- package/core/server/services/email-suppression-list/mailgun-email-suppression-list.js +6 -7
- package/core/server/services/gifts/README.md +2 -2
- package/core/server/services/gifts/constants.js +1 -1
- package/core/server/services/gifts/email-templates/gift-delivery.hbs +3 -3
- package/core/server/services/gifts/gift-email-service.js +0 -1
- package/core/server/services/gifts/gift-service.js +19 -57
- package/core/server/services/gifts/index.js +3 -5
- package/core/server/services/gifts/jobs/index.js +4 -4
- package/core/server/services/indexnow-ping/indexnow-ping-service.js +2 -2
- package/core/server/services/invitations/accept.js +4 -2
- package/core/server/services/jobs/job-service.js +0 -9
- package/core/server/services/jobs-service/index.js +8 -0
- package/core/server/services/jobs-service/jobs-service.js +52 -3
- package/core/server/services/jobs-service/register-job-handlers.js +32 -11
- package/core/server/services/mail/ghost-mailer.js +4 -4
- package/core/server/services/member-welcome-emails/service.js +7 -7
- package/core/server/services/members/account-service.js +98 -0
- package/core/server/services/members/api.js +4 -3
- package/core/server/services/members/import-export/csv/parse.js +0 -7
- package/core/server/services/members/import-export/export/exporter.js +20 -20
- package/core/server/services/members/import-export/import/completion-email.js +8 -11
- package/core/server/services/members/import-export/import/importer.js +13 -13
- package/core/server/services/members/import-export/import/row.js +0 -3
- package/core/server/services/members/import-export/index.js +10 -17
- package/core/server/services/members/jobs/clean-expired-comped-job.js +7 -0
- package/core/server/services/members/jobs/clean-expired-comped-task.js +145 -0
- package/core/server/services/members/jobs/index.js +48 -24
- package/core/server/services/members/members-api/controllers/member-controller.js +11 -0
- package/core/server/services/members/members-api/controllers/router-controller.js +3 -6
- package/core/server/services/members/members-api/members-api.js +26 -8
- package/core/server/services/members/members-api/repositories/event-repository.js +0 -4
- package/core/server/services/members/members-api/repositories/member-repository.js +4 -4
- package/core/server/services/members/members-api/services/member-bread-service.js +62 -84
- package/core/server/services/members/members-api/services/payments-service.js +1 -1
- package/core/server/services/members/members-ssr.js +18 -0
- package/core/server/services/members/middleware.js +95 -81
- package/core/server/services/members/service.js +71 -47
- package/core/server/services/members/utils.js +9 -0
- package/core/server/services/members-metafields/access.js +28 -0
- package/core/server/services/{members-custom-fields → members-metafields}/actions.js +5 -5
- package/core/server/services/members-metafields/bindings-service.js +147 -0
- package/core/server/services/members-metafields/codec.js +14 -0
- package/core/server/services/{members-custom-fields → members-metafields}/config.js +1 -1
- package/core/server/services/{members-custom-fields → members-metafields}/definitions-service.js +68 -22
- package/core/server/services/members-metafields/filter.js +181 -0
- package/core/server/services/{members-custom-fields → members-metafields}/index.js +16 -7
- package/core/server/services/{members-custom-fields → members-metafields}/key.js +2 -1
- package/core/server/services/{members-custom-fields → members-metafields}/models.js +5 -5
- package/core/server/services/members-metafields/namespaces.js +34 -0
- package/core/server/services/{members-custom-fields → members-metafields}/queries.js +1 -1
- package/core/server/services/{members-custom-fields → members-metafields}/schema.js +16 -12
- package/core/server/services/members-metafields/serializers.js +22 -0
- package/core/server/services/{members-custom-fields → members-metafields}/values-service.js +80 -48
- package/core/server/services/mentions/mention-controller.js +23 -21
- package/core/server/services/mentions/mention-sending-service.js +26 -9
- package/core/server/services/mentions/process-webmention-job.js +16 -0
- package/core/server/services/mentions/send-webmentions-job.js +18 -0
- package/core/server/services/mentions/service.js +7 -33
- package/core/server/services/route-settings/validation-errors.js +1 -1
- package/core/server/services/route-settings/yaml-parser.js +1 -1
- package/core/server/services/settings/settings-service.js +1 -8
- package/core/server/services/settings-helpers/settings-helpers.js +4 -6
- package/core/server/services/slack-ping/slack-ping-service.js +1 -1
- package/core/server/services/staff/staff-service-emails.js +2 -2
- package/core/server/services/stripe/config.js +14 -8
- package/core/server/services/stripe/service.js +10 -1
- package/core/server/services/stripe/services/checkout/completed-session.js +2 -2
- package/core/server/services/stripe/services/checkout/session-options.js +18 -15
- package/core/server/services/stripe/services/webhook/checkout-session-event-service.js +28 -7
- package/core/server/services/stripe/stripe-api.js +2 -0
- package/core/server/services/stripe/stripe-service.js +24 -14
- package/core/server/services/stripe/webhook-controller.js +10 -0
- package/core/server/services/tier-checkout-config/codec.js +11 -7
- package/core/server/services/tier-checkout-config/index.js +2 -1
- package/core/server/services/tier-checkout-config/models.js +11 -5
- package/core/server/services/tier-checkout-config/queries.js +12 -12
- package/core/server/services/tier-checkout-config/serializers.js +73 -10
- package/core/server/services/tier-checkout-config/service.js +14 -13
- package/core/server/services/tiers/service.js +3 -3
- package/core/server/services/update-check/index.js +13 -13
- package/core/server/services/update-check/jobs/update-check-job.js +7 -0
- package/core/server/services/update-check/update-check-service.js +2 -2
- package/core/server/services/url/router-filter.js +8 -10
- package/core/server/services/users.js +1 -19
- package/core/server/web/admin/controller.js +1 -1
- package/core/server/web/api/endpoints/admin/routes.js +20 -16
- package/core/server/web/members/account/index.js +2 -0
- package/core/server/web/members/account/routes.js +46 -0
- package/core/server/web/members/app.js +4 -18
- package/core/shared/config/defaults.json +1 -1
- package/core/shared/config/env/config.testing-mysql.json +1 -1
- package/core/shared/labs.js +7 -2
- package/package.json +42 -41
- package/pnpm-lock.yaml +1159 -582
- package/pnpm-workspace.yaml +18 -15
- package/components/tryghost-custom-field-types-0.0.0.tgz +0 -0
- package/components/tryghost-kg-default-nodes-2.2.0.tgz +0 -0
- package/core/built/admin/assets/Combination-5fYEzgbs.js +0 -41
- package/core/built/admin/assets/account-migration-DRhAehfw.js +0 -1
- package/core/built/admin/assets/activitypub/inbox-BluD2ucT.js +0 -1284
- package/core/built/admin/assets/activitypub/label-FHSh6ZAw.js +0 -16
- package/core/built/admin/assets/activitypub/layout-Cq4NG0Xr.js +0 -1
- package/core/built/admin/assets/activitypub/settings-DQDkr06X.js +0 -315
- package/core/built/admin/assets/add-integration-modal-BC0jExDZ.js +0 -1
- package/core/built/admin/assets/add-newsletter-modal-DchTkA7l.js +0 -1
- package/core/built/admin/assets/add-recommendation-modal-DMvaJZKp.js +0 -1
- package/core/built/admin/assets/alert-dialog-QhXzdHfc.js +0 -1
- package/core/built/admin/assets/analytics-No1DDY3A.js +0 -1
- package/core/built/admin/assets/automated-emails-te1D5kq-.js +0 -1
- package/core/built/admin/assets/automations-DpUNUdcZ.js +0 -1
- package/core/built/admin/assets/bluesky-sharing-Cjrv_f62.js +0 -1
- package/core/built/admin/assets/chunk.896.435fe0637944d4a69810.js +0 -366
- package/core/built/admin/assets/color-picker-BTY_NdEm.js +0 -3
- package/core/built/admin/assets/color-picker-field-CPlkzOH0.js +0 -1
- package/core/built/admin/assets/combobox-DrMtv_4P.js +0 -1
- package/core/built/admin/assets/command-ZdgYJpoI.js +0 -1
- package/core/built/admin/assets/comments-kidj60-h.js +0 -1
- package/core/built/admin/assets/confirmation-context-BhbmQQEl.js +0 -1
- package/core/built/admin/assets/custom-field-type-option-CsGIZk1J.js +0 -10
- package/core/built/admin/assets/custom-integration-modal-BY37O_9X.js +0 -1
- package/core/built/admin/assets/design-and-theme-modal-DUSKM4uC.js +0 -22
- package/core/built/admin/assets/dialog-B44iRwlc.js +0 -1
- package/core/built/admin/assets/dirty-confirm-dialog-BxgQgQ6H.js +0 -1
- package/core/built/admin/assets/dist-Cy8tfrp_.js +0 -1
- package/core/built/admin/assets/domain-Ca6KhoQt.js +0 -1
- package/core/built/admin/assets/dropzone-Bb-5zQTk.js +0 -5
- package/core/built/admin/assets/edit-profile-BdupLlus.js +0 -1
- package/core/built/admin/assets/editor-BP883Lpn.js +0 -8
- package/core/built/admin/assets/explore-FB-CimXw.js +0 -1
- package/core/built/admin/assets/feed-C1GxsmI2.js +0 -1
- package/core/built/admin/assets/filter-sources-D5n5bFA2.js +0 -1
- package/core/built/admin/assets/filters-DJEzkMUK.js +0 -1
- package/core/built/admin/assets/filters-MZXvdH5D.js +0 -1
- package/core/built/admin/assets/first-promoter-modal-CGTGLCr5.js +0 -1
- package/core/built/admin/assets/gift-link-modal-B6v0tnRN.js +0 -1
- package/core/built/admin/assets/growth-BGAJD1rs.js +0 -3
- package/core/built/admin/assets/history-modal-iKwUY3XR.js +0 -1
- package/core/built/admin/assets/hooks-CuEZV2-q.js +0 -2
- package/core/built/admin/assets/index-DjQe-e5s.js +0 -4
- package/core/built/admin/assets/index-Dqf2x-ry.css +0 -1
- package/core/built/admin/assets/index.es-VihPwfeH.js +0 -1
- package/core/built/admin/assets/input--W-rCTrX.js +0 -1
- package/core/built/admin/assets/invite-user-modal-MdImLCaJ.js +0 -1
- package/core/built/admin/assets/koenig-loader-DYZpm6El.js +0 -2
- package/core/built/admin/assets/label-picker-cB51xl1r.js +0 -1
- package/core/built/admin/assets/layout-c_B_1hax.js +0 -1
- package/core/built/admin/assets/link-helpers-MVDdnFCY.js +0 -1
- package/core/built/admin/assets/loading-indicator-BpFWTVgh.js +0 -1
- package/core/built/admin/assets/member-custom-fields-qs0dIkxM.js +0 -1
- package/core/built/admin/assets/member-detail-B6uec2a6.js +0 -1
- package/core/built/admin/assets/members-BQF3CqhI.js +0 -2
- package/core/built/admin/assets/moderation-DY4wF1Q3.js +0 -1
- package/core/built/admin/assets/navigation-modal-DX-gNF3X.js +0 -1
- package/core/built/admin/assets/newsletter-CyQA7m3m.js +0 -1
- package/core/built/admin/assets/newsletter-detail-modal-BDrptuxn.js +0 -1
- package/core/built/admin/assets/newsletters-DXuNmC42.js +0 -1
- package/core/built/admin/assets/newsletters-DhNigJUh.js +0 -1
- package/core/built/admin/assets/note-Cgo2LCJ6.js +0 -1
- package/core/built/admin/assets/notifications-BC50AMku.js +0 -1
- package/core/built/admin/assets/offers-container-modal-DWycIMzT.js +0 -1
- package/core/built/admin/assets/onboarding-DGQuV7Ew.js +0 -1
- package/core/built/admin/assets/overview-CVbFtn-k.js +0 -1
- package/core/built/admin/assets/overview-YKj7SZaK.js +0 -1
- package/core/built/admin/assets/pages-B6vr9F_D.js +0 -1
- package/core/built/admin/assets/popover-CCy0h5rq.js +0 -1
- package/core/built/admin/assets/portal-frame-dc4XKr0e.js +0 -1
- package/core/built/admin/assets/portal-modal-BvvdF_B9.js +0 -1
- package/core/built/admin/assets/post-analytics-DX470WRG.js +0 -1
- package/core/built/admin/assets/post-analytics-header-CXpJmdDv.js +0 -1
- package/core/built/admin/assets/post-analytics-provider-CLfKGWZy.js +0 -1
- package/core/built/admin/assets/post-helpers-C5OQMBoi.js +0 -1
- package/core/built/admin/assets/post-share-modal-Cl0NXRJr.js +0 -1
- package/core/built/admin/assets/posts-KY3aUhVV.js +0 -1
- package/core/built/admin/assets/preferences-DO0UUWFp.js +0 -5
- package/core/built/admin/assets/profile-CxT-0l4d.js +0 -1
- package/core/built/admin/assets/router-provider-B0gZyiGJ.js +0 -1
- package/core/built/admin/assets/schemas-4UPic-oD.js +0 -64
- package/core/built/admin/assets/select-DuDUFdPq.js +0 -1
- package/core/built/admin/assets/settings-BjCii7BM.js +0 -1
- package/core/built/admin/assets/settings-b8kNzZSQ.js +0 -4
- package/core/built/admin/assets/share-modal-BV70mr9k.js +0 -1
- package/core/built/admin/assets/shim-ClF_m5ux.js +0 -1
- package/core/built/admin/assets/slack-modal-Be2_KK-z.js +0 -1
- package/core/built/admin/assets/stats-C4reGh9O.js +0 -1
- package/core/built/admin/assets/stats-view-DP9pQ62k.js +0 -1
- package/core/built/admin/assets/stripe-connect-modal-Z-UT_BGc.js +0 -1
- package/core/built/admin/assets/tag-detail-Dc0nTOtU.js +0 -2
- package/core/built/admin/assets/tags-BLaXBwnb.js +0 -1
- package/core/built/admin/assets/testimonials-modal-sFwRnNLh.js +0 -1
- package/core/built/admin/assets/tier-detail-modal-BCFpWwUS.js +0 -1
- package/core/built/admin/assets/tiers-Nd_z4BZY.js +0 -1
- package/core/built/admin/assets/use-filter-params-CAvfDvf1.js +0 -1
- package/core/built/admin/assets/use-growth-stats-D6mZXGhK.js +0 -1
- package/core/built/admin/assets/use-host-limits-DZtR08oa.js +0 -1
- package/core/built/admin/assets/use-pintura-config-CzTfnufz.js +0 -1
- package/core/built/admin/assets/use-reply-chain-data-BeQwz8oF.js +0 -1
- package/core/built/admin/assets/use-settings-navigation-BbrSUVcf.js +0 -1
- package/core/built/admin/assets/use-staff-users-cZg4ugOF.js +0 -1
- package/core/built/admin/assets/use-tinybird-query-B1IMyAcv.js +0 -39
- package/core/built/admin/assets/use-tinybird-token-Bow0Zwn8.js +0 -1
- package/core/built/admin/assets/use-upgrade-route-DhRSo7at.js +0 -1
- package/core/built/admin/assets/user-detail-modal-yipSoQfv.js +0 -1
- package/core/built/admin/assets/users-5msE1WMs.js +0 -1
- package/core/built/admin/assets/web-CKcXAu86.js +0 -1
- package/core/built/admin/assets/web-DR4TqM3E.js +0 -2
- package/core/server/api/endpoints/member-custom-fields.js +0 -90
- package/core/server/api/endpoints/utils/serializers/output/member-custom-fields.js +0 -20
- package/core/server/services/email-suppression-list/in-memory-email-suppression-list.js +0 -39
- package/core/server/services/jobs/worker-model-event-bridge.js +0 -149
- package/core/server/services/members/jobs/clean-expired-comped.js +0 -132
- package/core/server/services/members-custom-fields/bindings-service.js +0 -119
- package/core/server/services/members-custom-fields/codec.js +0 -15
- package/core/server/services/members-custom-fields/filter.js +0 -199
- package/core/server/services/members-custom-fields/serializers.js +0 -24
- package/core/server/services/stripe/services/checkout/allowed-countries.js +0 -267
- package/core/server/services/stripe/services/checkout/field-ports.js +0 -34
- package/core/server/services/tier-checkout-config/destinations.js +0 -17
- package/core/server/services/update-check/run-update-check.js +0 -63
- /package/core/built/admin/assets/{chunk.492.f24d855d7e3f3aad261c.js.LICENSE.txt → chunk.901.4ccc639821020230bcca.js.LICENSE.txt} +0 -0
- /package/core/server/services/{members-custom-fields → members-metafields}/storage.js +0 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Email service
|
|
2
|
+
|
|
3
|
+
Renders newsletter emails, splits them into recipient batches, and submits the
|
|
4
|
+
batches to the configured email provider. Domain terms live in
|
|
5
|
+
[CONTEXT.md](CONTEXT.md).
|
|
6
|
+
|
|
7
|
+
## Sending status
|
|
8
|
+
|
|
9
|
+
The sending status served by the Admin API's `emails/:id/status` endpoint is
|
|
10
|
+
derived on read. `sending-status.ts` owns the derivation from an email and its
|
|
11
|
+
batches with their recipient counts; `SendingStatusService` reads those rows
|
|
12
|
+
and `sending-status-serializers.ts` shapes the response. Submitted is terminal and the batch aggregation only
|
|
13
|
+
describes a send that is still in progress or has failed, so submitted emails answer with the email's stored
|
|
14
|
+
`email_count` as both completed and total; batch creation reconciles that
|
|
15
|
+
column to the recipient rows it built. That also keeps reads of long-finished
|
|
16
|
+
emails cheap, with no query over the batches or recipients. The endpoint is always available; Admin decides
|
|
17
|
+
whether to poll it while a send is active.
|
|
18
|
+
|
|
19
|
+
The phase is read from the batches: an email is submitting once any batch has
|
|
20
|
+
left `pending`, and preparing otherwise. Batch statuses persist across
|
|
21
|
+
attempts, so a retried email reports submitting with frozen progress while it
|
|
22
|
+
waits for its job, and `failed_during` is the same derivation applied to a
|
|
23
|
+
failed email.
|
|
24
|
+
|
|
25
|
+
The rough ETA measures recent recipient throughput, including work completed by
|
|
26
|
+
concurrent workers. It stays `null` until enough timing samples are available in
|
|
27
|
+
the current phase and attempt, so short sends may finish without showing an ETA.
|
|
28
|
+
Progress counts update independently of the estimate.
|
|
29
|
+
|
|
30
|
+
The ETA is always `null` for failed emails and `0` once no work remains in the
|
|
31
|
+
current phase. Batches that failed during the current attempt are not remaining
|
|
32
|
+
work until the email is retried, so the ETA can reach `0` while completed is
|
|
33
|
+
still below total. Consumers should key completion on the status, never the ETA.
|
|
34
|
+
|
|
35
|
+
Ghost does not record when a sending attempt or phase started, so the ETA uses
|
|
36
|
+
the email's `updated_at` as a proxy for the start of the current attempt: the
|
|
37
|
+
sending job saves the email when it takes its status lock and, when the
|
|
38
|
+
recipient count changed, again after creating batches; a retry or boot resume
|
|
39
|
+
saves it when re-queuing the email. Batches completed before that timestamp
|
|
40
|
+
belong to an earlier attempt and are left out of the rate window. The proxy
|
|
41
|
+
only holds while nothing saves the Email model, or sets `emails.updated_at`
|
|
42
|
+
through a raw update, while batches are being submitted.
|
|
@@ -33,7 +33,6 @@ class BatchSendingService {
|
|
|
33
33
|
#models;
|
|
34
34
|
#db;
|
|
35
35
|
#sentry;
|
|
36
|
-
#debugStorageFilePath;
|
|
37
36
|
#getRequiredUrlRelations;
|
|
38
37
|
#shuttingDown = false;
|
|
39
38
|
#inFlight = new Set();
|
|
@@ -68,7 +67,6 @@ class BatchSendingService {
|
|
|
68
67
|
* @param {object} [dependencies.BEFORE_RETRY_CONFIG]
|
|
69
68
|
* @param {object} [dependencies.AFTER_RETRY_CONFIG]
|
|
70
69
|
* @param {object} [dependencies.MAILGUN_API_RETRY_CONFIG]
|
|
71
|
-
* @param {string} [dependencies.debugStorageFilePath]
|
|
72
70
|
*/
|
|
73
71
|
constructor({
|
|
74
72
|
emailRenderer,
|
|
@@ -83,7 +81,6 @@ class BatchSendingService {
|
|
|
83
81
|
BEFORE_RETRY_CONFIG,
|
|
84
82
|
AFTER_RETRY_CONFIG,
|
|
85
83
|
MAILGUN_API_RETRY_CONFIG,
|
|
86
|
-
debugStorageFilePath,
|
|
87
84
|
}) {
|
|
88
85
|
this.#emailRenderer = emailRenderer;
|
|
89
86
|
this.#sendingService = sendingService;
|
|
@@ -93,7 +90,6 @@ class BatchSendingService {
|
|
|
93
90
|
this.#models = models;
|
|
94
91
|
this.#db = db;
|
|
95
92
|
this.#sentry = sentry;
|
|
96
|
-
this.#debugStorageFilePath = debugStorageFilePath;
|
|
97
93
|
this.#getRequiredUrlRelations = getRequiredUrlRelations;
|
|
98
94
|
|
|
99
95
|
if (BEFORE_RETRY_CONFIG) {
|
|
@@ -217,6 +213,7 @@ class BatchSendingService {
|
|
|
217
213
|
|
|
218
214
|
// Check if email is 'pending' only + change status to submitting in one transaction.
|
|
219
215
|
// This allows us to have a lock around the email job that makes sure an email can only have one active job.
|
|
216
|
+
// Also stamps updated_at, which SendingStatusService reads as the attempt start; do not save the Email once batches submit.
|
|
220
217
|
let email;
|
|
221
218
|
try {
|
|
222
219
|
email = await this.retryDb(
|
|
@@ -708,6 +705,7 @@ class BatchSendingService {
|
|
|
708
705
|
const deliveryTimes = this.calculateDeliveryTimes(email, batches.length);
|
|
709
706
|
|
|
710
707
|
// Loop batches and send them via the EmailProvider
|
|
708
|
+
// SendingStatusService treats a batch that fails in this run as finished work; never re-queue it within the run.
|
|
711
709
|
let succeededCount = 0;
|
|
712
710
|
const queue = batches.slice();
|
|
713
711
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
const errors = require('@tryghost/errors');
|
|
2
2
|
const tpl = require('@tryghost/tpl');
|
|
3
3
|
|
|
4
|
+
/** @typedef {import('./sending-status-service').SendingStatusService} SendingStatusService */
|
|
5
|
+
|
|
4
6
|
const messages = {
|
|
5
7
|
postNotFound: 'Post not found.',
|
|
6
8
|
noEmailsProvided: 'No emails provided.',
|
|
@@ -23,16 +25,18 @@ class EmailController {
|
|
|
23
25
|
service;
|
|
24
26
|
models;
|
|
25
27
|
getRequiredUrlRelations;
|
|
28
|
+
sendingStatusService;
|
|
26
29
|
|
|
27
30
|
/**
|
|
28
31
|
*
|
|
29
32
|
* @param {EmailService} service
|
|
30
|
-
* @param {{models: {Post: any, Newsletter: any, Email: any}, getRequiredUrlRelations?: () => string[]}} dependencies
|
|
33
|
+
* @param {{models: {Post: any, Newsletter: any, Email: any}, getRequiredUrlRelations?: () => string[], sendingStatusService: SendingStatusService}} dependencies
|
|
31
34
|
*/
|
|
32
|
-
constructor(service, { models, getRequiredUrlRelations = () => [] }) {
|
|
35
|
+
constructor(service, { models, getRequiredUrlRelations = () => [], sendingStatusService }) {
|
|
33
36
|
this.service = service;
|
|
34
37
|
this.models = models;
|
|
35
38
|
this.getRequiredUrlRelations = getRequiredUrlRelations;
|
|
39
|
+
this.sendingStatusService = sendingStatusService;
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
async _getFrameData(frame) {
|
|
@@ -143,6 +147,18 @@ class EmailController {
|
|
|
143
147
|
|
|
144
148
|
return await this.service.retryEmail(email);
|
|
145
149
|
}
|
|
150
|
+
|
|
151
|
+
async getSendingStatus(frame) {
|
|
152
|
+
const status = await this.sendingStatusService.statusFor(frame.data.id);
|
|
153
|
+
|
|
154
|
+
if (!status) {
|
|
155
|
+
throw new errors.NotFoundError({
|
|
156
|
+
message: tpl(messages.emailNotFound),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return status;
|
|
161
|
+
}
|
|
146
162
|
}
|
|
147
163
|
|
|
148
164
|
module.exports = EmailController;
|
|
@@ -13,7 +13,7 @@ function isUnsplashImage(url) {
|
|
|
13
13
|
}
|
|
14
14
|
const { DateTime } = require('luxon');
|
|
15
15
|
const htmlToPlaintext = require('@tryghost/html-to-plaintext');
|
|
16
|
-
const
|
|
16
|
+
const emailAddressParser = require('../email-address/email-address-parser');
|
|
17
17
|
const { getEmailDesign } = require('../email-rendering/email-design');
|
|
18
18
|
const { registerHelpers } = require('./helpers/register-helpers');
|
|
19
19
|
const crypto = require('crypto');
|
|
@@ -229,6 +229,8 @@ function cheerioLoad(html) {
|
|
|
229
229
|
* @prop {string} id
|
|
230
230
|
* @prop {RegExp} token
|
|
231
231
|
* @prop {(member: MemberLike) => string} getValue
|
|
232
|
+
* @prop {boolean} [trusted] - Value is server-generated, so it is inserted into the
|
|
233
|
+
* html body as-is instead of being HTML-escaped. Defaults to false (escaped).
|
|
232
234
|
*/
|
|
233
235
|
|
|
234
236
|
/**
|
|
@@ -337,7 +339,7 @@ class EmailRenderer {
|
|
|
337
339
|
}
|
|
338
340
|
|
|
339
341
|
#getRawFromAddress(post, newsletter) {
|
|
340
|
-
// Pass the raw name through;
|
|
342
|
+
// Pass the raw name through; emailAddressParser.stringify() is the single
|
|
341
343
|
// point that escapes it for the RFC5322 quoted-string From header. Escaping
|
|
342
344
|
// here too would double-escape (e.g. a title containing a double quote).
|
|
343
345
|
let senderName = this.#settingsCache.get('title') || '';
|
|
@@ -394,7 +396,7 @@ class EmailRenderer {
|
|
|
394
396
|
{ useFallbackAddress },
|
|
395
397
|
);
|
|
396
398
|
|
|
397
|
-
return
|
|
399
|
+
return emailAddressParser.stringify(addresses.from);
|
|
398
400
|
}
|
|
399
401
|
|
|
400
402
|
/**
|
|
@@ -423,7 +425,7 @@ class EmailRenderer {
|
|
|
423
425
|
);
|
|
424
426
|
|
|
425
427
|
if (addresses.replyTo) {
|
|
426
|
-
return
|
|
428
|
+
return emailAddressParser.stringify(addresses.replyTo);
|
|
427
429
|
}
|
|
428
430
|
return null;
|
|
429
431
|
}
|
|
@@ -920,18 +922,21 @@ class EmailRenderer {
|
|
|
920
922
|
getValue: (member) => {
|
|
921
923
|
return this.createUnsubscribeUrl(member.uuid, { newsletterUuid });
|
|
922
924
|
},
|
|
925
|
+
trusted: true, // Server-generated URL, must not be HTML-escaped
|
|
923
926
|
},
|
|
924
927
|
{
|
|
925
928
|
id: 'manage_account_url',
|
|
926
929
|
getValue: () => {
|
|
927
930
|
return this.createManageAccountUrl();
|
|
928
931
|
},
|
|
932
|
+
trusted: true, // Server-generated URL, must not be HTML-escaped
|
|
929
933
|
},
|
|
930
934
|
{
|
|
931
935
|
id: 'uuid',
|
|
932
936
|
getValue: (member) => {
|
|
933
937
|
return member.uuid;
|
|
934
938
|
},
|
|
939
|
+
trusted: true, // Server-generated identifier
|
|
935
940
|
},
|
|
936
941
|
{
|
|
937
942
|
id: 'key',
|
|
@@ -941,6 +946,7 @@ class EmailRenderer {
|
|
|
941
946
|
.update(member.uuid)
|
|
942
947
|
.digest('hex');
|
|
943
948
|
},
|
|
949
|
+
trusted: true, // Server-generated hmac
|
|
944
950
|
},
|
|
945
951
|
{
|
|
946
952
|
id: 'first_name',
|
|
@@ -1000,6 +1006,7 @@ class EmailRenderer {
|
|
|
1000
1006
|
return this.createUnsubscribeUrl(member.uuid, { newsletterUuid });
|
|
1001
1007
|
},
|
|
1002
1008
|
required: true, // Used in email headers
|
|
1009
|
+
trusted: true, // Server-generated URL, must not be HTML-escaped
|
|
1003
1010
|
},
|
|
1004
1011
|
// Unique ID used for ad images to bypass ESP image proxies
|
|
1005
1012
|
{
|
|
@@ -1007,6 +1014,7 @@ class EmailRenderer {
|
|
|
1007
1014
|
getValue: () => {
|
|
1008
1015
|
return crypto.randomUUID();
|
|
1009
1016
|
},
|
|
1017
|
+
trusted: true, // Server-generated identifier
|
|
1010
1018
|
},
|
|
1011
1019
|
];
|
|
1012
1020
|
|
|
@@ -1043,6 +1051,7 @@ class EmailRenderer {
|
|
|
1043
1051
|
getValue: fallback
|
|
1044
1052
|
? (member) => definition.getValue(member) || fallback
|
|
1045
1053
|
: definition.getValue,
|
|
1054
|
+
trusted: definition.trusted === true,
|
|
1046
1055
|
});
|
|
1047
1056
|
}
|
|
1048
1057
|
}
|
|
@@ -1056,6 +1065,7 @@ class EmailRenderer {
|
|
|
1056
1065
|
originalId: definition.id,
|
|
1057
1066
|
token: new RegExp(`%%\\{${definition.id}\\}%%`, 'g'),
|
|
1058
1067
|
getValue: definition.getValue,
|
|
1068
|
+
trusted: definition.trusted === true,
|
|
1059
1069
|
});
|
|
1060
1070
|
}
|
|
1061
1071
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const debug = require('@tryghost/debug')('i18n');
|
|
2
|
-
const logging = require('@tryghost/logging');
|
|
3
2
|
const url = require('../../api/endpoints/utils/serializers/output/utils/url');
|
|
4
3
|
const events = require('../../lib/common/events');
|
|
5
4
|
|
|
@@ -25,6 +24,7 @@ class EmailServiceWrapper {
|
|
|
25
24
|
const EmailRenderer = require('./email-renderer');
|
|
26
25
|
const SendingService = require('./sending-service');
|
|
27
26
|
const BatchSendingService = require('./batch-sending-service');
|
|
27
|
+
const { SendingStatusService } = require('./sending-status-service');
|
|
28
28
|
const EmailSegmenter = require('./email-segmenter');
|
|
29
29
|
const MailgunEmailProvider = require('./mailgun-email-provider');
|
|
30
30
|
const { DomainWarmingService } = require('./domain-warming-service');
|
|
@@ -55,12 +55,6 @@ class EmailServiceWrapper {
|
|
|
55
55
|
const emailAnalyticsJobs = require('../email-analytics/jobs');
|
|
56
56
|
const { cachedImageSizeFromUrl } = require('../../lib/image');
|
|
57
57
|
|
|
58
|
-
// capture errors from mailgun client and log them in sentry
|
|
59
|
-
const errorHandler = (error) => {
|
|
60
|
-
logging.info(`Capturing error for mailgun email provider service`);
|
|
61
|
-
sentry.captureException(error);
|
|
62
|
-
};
|
|
63
|
-
|
|
64
58
|
// Mailgun client instance for email provider
|
|
65
59
|
const mailgunClient = new MailgunClient({
|
|
66
60
|
config: configService,
|
|
@@ -78,7 +72,6 @@ class EmailServiceWrapper {
|
|
|
78
72
|
const mailgunEmailProvider = new MailgunEmailProvider({
|
|
79
73
|
mailgunClient,
|
|
80
74
|
config: configService,
|
|
81
|
-
errorHandler,
|
|
82
75
|
});
|
|
83
76
|
|
|
84
77
|
const emailRenderer = new EmailRenderer({
|
|
@@ -134,8 +127,8 @@ class EmailServiceWrapper {
|
|
|
134
127
|
db,
|
|
135
128
|
sentry,
|
|
136
129
|
getRequiredUrlRelations,
|
|
137
|
-
debugStorageFilePath: configService.getContentPath('data'),
|
|
138
130
|
});
|
|
131
|
+
const sendingStatusService = new SendingStatusService({ knex: db.knex });
|
|
139
132
|
|
|
140
133
|
if (ghostServer) {
|
|
141
134
|
// Two phases: stop claiming batches immediately, drain in-flight ones later.
|
|
@@ -178,6 +171,7 @@ class EmailServiceWrapper {
|
|
|
178
171
|
Email,
|
|
179
172
|
},
|
|
180
173
|
getRequiredUrlRelations,
|
|
174
|
+
sendingStatusService,
|
|
181
175
|
});
|
|
182
176
|
}
|
|
183
177
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const logging = require('@tryghost/logging');
|
|
2
2
|
const errors = require('@tryghost/errors');
|
|
3
3
|
const debug = require('@tryghost/debug')('email-service:mailgun-provider-service');
|
|
4
|
+
const { escapeExpression } = require('handlebars');
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* @typedef {object} Config
|
|
@@ -35,35 +36,41 @@ const debug = require('@tryghost/debug')('email-service:mailgun-provider-service
|
|
|
35
36
|
class MailgunEmailProvider {
|
|
36
37
|
#mailgunClient;
|
|
37
38
|
#config;
|
|
38
|
-
#errorHandler;
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* @param {object} dependencies
|
|
42
42
|
* @param {import('../lib/mailgun-client')} dependencies.mailgunClient - mailgun client to send emails
|
|
43
43
|
* @param {Config} dependencies.config
|
|
44
|
-
* @param {Function} [dependencies.errorHandler] - custom error handler for logging exceptions
|
|
45
44
|
*/
|
|
46
|
-
constructor({ mailgunClient, config
|
|
45
|
+
constructor({ mailgunClient, config }) {
|
|
47
46
|
this.#mailgunClient = mailgunClient;
|
|
48
47
|
this.#config = config;
|
|
49
|
-
this.#errorHandler = errorHandler;
|
|
50
48
|
}
|
|
51
49
|
|
|
52
|
-
#createRecipientData(replacements) {
|
|
50
|
+
#createRecipientData(replacements, htmlEscapedIds) {
|
|
53
51
|
let recipientData = {};
|
|
54
52
|
|
|
55
53
|
recipientData = replacements.reduce((acc, replacement) => {
|
|
56
54
|
const { id, value } = replacement;
|
|
57
55
|
acc[id] = value;
|
|
56
|
+
// Mailgun's recipient-variables substitution is a simple string replace with
|
|
57
|
+
// no HTML awareness, and the same value is used for both the html and plaintext
|
|
58
|
+
// bodies. Member-controlled values get an extra, escaped variant that only the
|
|
59
|
+
// html body points at. Trusted server-generated values (urls, uuids, hmacs) are
|
|
60
|
+
// skipped so their URLs don't end up entity-encoded inside hrefs.
|
|
61
|
+
if (htmlEscapedIds.has(id)) {
|
|
62
|
+
acc[`${id}_html`] = typeof value === 'string' ? escapeExpression(value) : value;
|
|
63
|
+
}
|
|
58
64
|
return acc;
|
|
59
65
|
}, {});
|
|
60
66
|
|
|
61
67
|
return recipientData;
|
|
62
68
|
}
|
|
63
69
|
|
|
64
|
-
#updateRecipientVariables(data, replacementDefinitions) {
|
|
70
|
+
#updateRecipientVariables(data, replacementDefinitions, isHtml) {
|
|
65
71
|
for (const def of replacementDefinitions) {
|
|
66
|
-
|
|
72
|
+
const useHtmlVariant = isHtml && !def.trusted;
|
|
73
|
+
data = data.replace(def.token, `%recipient.${def.id}${useHtmlVariant ? '_html' : ''}%`);
|
|
67
74
|
}
|
|
68
75
|
return data;
|
|
69
76
|
}
|
|
@@ -128,8 +135,11 @@ class MailgunEmailProvider {
|
|
|
128
135
|
}
|
|
129
136
|
|
|
130
137
|
// create recipient data for Mailgun using replacement definitions
|
|
138
|
+
const htmlEscapedIds = new Set(
|
|
139
|
+
replacementDefinitions.filter((def) => !def.trusted).map((def) => def.id),
|
|
140
|
+
);
|
|
131
141
|
const recipientData = recipients.reduce((acc, recipient) => {
|
|
132
|
-
acc[recipient.email] = this.#createRecipientData(recipient.replacements);
|
|
142
|
+
acc[recipient.email] = this.#createRecipientData(recipient.replacements, htmlEscapedIds);
|
|
133
143
|
return acc;
|
|
134
144
|
}, {});
|
|
135
145
|
|
|
@@ -139,6 +149,7 @@ class MailgunEmailProvider {
|
|
|
139
149
|
messageData[key] = this.#updateRecipientVariables(
|
|
140
150
|
messageData[key],
|
|
141
151
|
replacementDefinitions,
|
|
152
|
+
key === 'html',
|
|
142
153
|
);
|
|
143
154
|
}
|
|
144
155
|
});
|
|
@@ -165,7 +176,7 @@ class MailgunEmailProvider {
|
|
|
165
176
|
message: this.#createMailgunErrorMessage(error),
|
|
166
177
|
errorDetails: JSON.stringify({ error, messageData }),
|
|
167
178
|
context: `Mailgun Error ${error.status}: ${error.details}`,
|
|
168
|
-
help: `https://ghost.org/
|
|
179
|
+
help: `https://docs.ghost.org/newsletters/#bulk-email-configuration`,
|
|
169
180
|
code: 'BULK_EMAIL_SEND_FAILED',
|
|
170
181
|
});
|
|
171
182
|
} else {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DbBatchSendingRow = exports.DbEmailSendingRow = exports.StoredSendingStatus = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const count_1 = require("../../lib/db-types/count");
|
|
6
|
+
const date_1 = require("../../lib/db-types/date");
|
|
7
|
+
exports.StoredSendingStatus = zod_1.z.enum(['pending', 'submitting', 'submitted', 'failed']);
|
|
8
|
+
// Projections of tables the Bookshelf models own, not full rows, so there is no knex table
|
|
9
|
+
// augmentation here: typing `emails` by these columns alone would mistype every other read.
|
|
10
|
+
exports.DbEmailSendingRow = zod_1.z.object({
|
|
11
|
+
id: zod_1.z.string(),
|
|
12
|
+
status: exports.StoredSendingStatus,
|
|
13
|
+
email_count: count_1.DbCount,
|
|
14
|
+
updated_at: date_1.DbDate.nullable(),
|
|
15
|
+
});
|
|
16
|
+
exports.DbBatchSendingRow = zod_1.z.object({
|
|
17
|
+
status: exports.StoredSendingStatus,
|
|
18
|
+
created_at: date_1.DbDate,
|
|
19
|
+
updated_at: date_1.DbDate,
|
|
20
|
+
recipient_count: count_1.DbCount,
|
|
21
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toEmailStatusesResponse = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const case_keys_1 = require("../../lib/case-keys");
|
|
6
|
+
const sending_status_1 = require("./sending-status");
|
|
7
|
+
const SendingPhaseResource = zod_1.z.enum(['preparing', 'submitting']);
|
|
8
|
+
const SendingResource = zod_1.z.object({
|
|
9
|
+
status: zod_1.z.enum(['preparing', 'submitting', 'submitted', 'failed']),
|
|
10
|
+
progress: zod_1.z.object({
|
|
11
|
+
completed: zod_1.z.number(),
|
|
12
|
+
total: zod_1.z.number(),
|
|
13
|
+
estimated_seconds_remaining: zod_1.z.number().nullable(),
|
|
14
|
+
}),
|
|
15
|
+
failed_during: SendingPhaseResource.optional(),
|
|
16
|
+
});
|
|
17
|
+
const EmailStatusResource = zod_1.z.object({
|
|
18
|
+
id: zod_1.z.string(),
|
|
19
|
+
sending: SendingResource,
|
|
20
|
+
});
|
|
21
|
+
const EmailStatusesResponse = zod_1.z.object({ email_statuses: zod_1.z.array(EmailStatusResource) });
|
|
22
|
+
exports.toEmailStatusesResponse = sending_status_1.EmailSendingStatus.transform(({ id, sending }) => ({
|
|
23
|
+
email_statuses: [
|
|
24
|
+
{
|
|
25
|
+
id,
|
|
26
|
+
sending: {
|
|
27
|
+
status: sending.status,
|
|
28
|
+
progress: (0, case_keys_1.snakeKeys)(sending.progress),
|
|
29
|
+
...(sending.status === 'failed' ? { failed_during: sending.failedDuring } : {}),
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
})).pipe(EmailStatusesResponse);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SendingStatusService = void 0;
|
|
4
|
+
const case_keys_1 = require("../../lib/case-keys");
|
|
5
|
+
const sending_status_schema_1 = require("./sending-status-schema");
|
|
6
|
+
const sending_status_1 = require("./sending-status");
|
|
7
|
+
class SendingStatusService {
|
|
8
|
+
#knex;
|
|
9
|
+
constructor({ knex }) {
|
|
10
|
+
this.#knex = knex;
|
|
11
|
+
}
|
|
12
|
+
async statusFor(emailId) {
|
|
13
|
+
const row = await this.#knex('emails')
|
|
14
|
+
.select('id', 'status', 'email_count', 'updated_at')
|
|
15
|
+
.where('id', emailId)
|
|
16
|
+
.first();
|
|
17
|
+
if (!row) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
const email = sending_status_schema_1.DbEmailSendingRow.parse(row);
|
|
21
|
+
// A submitted email answers from its own count, and batch creation reconciles email_count
|
|
22
|
+
// to the recipient rows it built, so the batch query is skipped rather than run and ignored.
|
|
23
|
+
const batches = email.status === 'submitted' ? [] : await this.#batchesFor(emailId);
|
|
24
|
+
return {
|
|
25
|
+
id: email.id,
|
|
26
|
+
sending: (0, sending_status_1.buildSendingStatus)({
|
|
27
|
+
status: email.status,
|
|
28
|
+
recipientCount: email.email_count,
|
|
29
|
+
// The sending job saves the email when it takes its status lock, so updated_at
|
|
30
|
+
// stands in for the attempt start that Ghost does not record.
|
|
31
|
+
attemptStartedAt: email.updated_at,
|
|
32
|
+
}, batches),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
async #batchesFor(emailId) {
|
|
36
|
+
// Correlated per-batch count stays on the batch_id index; grouping recipients by email_id scans every recipient row.
|
|
37
|
+
const recipientCount = this.#knex('email_recipients as recipient')
|
|
38
|
+
.count('*')
|
|
39
|
+
.whereRaw('recipient.batch_id = batch.id');
|
|
40
|
+
const rows = await this.#knex('email_batches as batch')
|
|
41
|
+
.select('batch.status', 'batch.created_at', 'batch.updated_at')
|
|
42
|
+
.select(recipientCount.as('recipient_count'))
|
|
43
|
+
.where('batch.email_id', emailId);
|
|
44
|
+
return rows.map((batchRow) => (0, case_keys_1.camelKeys)(sending_status_schema_1.DbBatchSendingRow.parse(batchRow)));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
exports.SendingStatusService = SendingStatusService;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EmailSendingStatus = exports.SendingStatus = exports.SendingProgress = exports.SendingPhase = void 0;
|
|
4
|
+
exports.buildSendingStatus = buildSendingStatus;
|
|
5
|
+
const zod_1 = require("zod");
|
|
6
|
+
const ETA_COMPLETION_WINDOW = 20;
|
|
7
|
+
const ETA_MIN_INTERVALS = 2;
|
|
8
|
+
exports.SendingPhase = zod_1.z.enum(['preparing', 'submitting']);
|
|
9
|
+
exports.SendingProgress = zod_1.z.object({
|
|
10
|
+
completed: zod_1.z.number().int().nonnegative(),
|
|
11
|
+
total: zod_1.z.number().int().nonnegative(),
|
|
12
|
+
estimatedSecondsRemaining: zod_1.z.number().int().nonnegative().nullable(),
|
|
13
|
+
});
|
|
14
|
+
exports.SendingStatus = zod_1.z.discriminatedUnion('status', [
|
|
15
|
+
zod_1.z.object({ status: exports.SendingPhase, progress: exports.SendingProgress }),
|
|
16
|
+
zod_1.z.object({ status: zod_1.z.literal('submitted'), progress: exports.SendingProgress }),
|
|
17
|
+
zod_1.z.object({ status: zod_1.z.literal('failed'), progress: exports.SendingProgress, failedDuring: exports.SendingPhase }),
|
|
18
|
+
]);
|
|
19
|
+
exports.EmailSendingStatus = zod_1.z.object({
|
|
20
|
+
id: zod_1.z.string(),
|
|
21
|
+
sending: exports.SendingStatus,
|
|
22
|
+
});
|
|
23
|
+
function buildSendingStatus(email, batches) {
|
|
24
|
+
// Submitted is terminal: every batch was accepted, so the email's own recipient count is the
|
|
25
|
+
// answer and the batch aggregation below, which describes a send still in progress, does not apply.
|
|
26
|
+
if (email.status === 'submitted') {
|
|
27
|
+
return {
|
|
28
|
+
status: 'submitted',
|
|
29
|
+
progress: {
|
|
30
|
+
completed: email.recipientCount,
|
|
31
|
+
total: email.recipientCount,
|
|
32
|
+
estimatedSecondsRemaining: 0,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const phase = batches.some((batch) => batch.status !== 'pending')
|
|
37
|
+
? 'submitting'
|
|
38
|
+
: 'preparing';
|
|
39
|
+
const attemptStartedAt = email.attemptStartedAt?.getTime() ?? null;
|
|
40
|
+
const preparedCount = sumRecipients(batches);
|
|
41
|
+
const completedBatches = phase === 'preparing' ? batches : batches.filter((batch) => batch.status === 'submitted');
|
|
42
|
+
const completed = sumRecipients(completedBatches);
|
|
43
|
+
const total = phase === 'preparing' ? Math.max(email.recipientCount, preparedCount) : preparedCount;
|
|
44
|
+
if (email.status === 'failed') {
|
|
45
|
+
return {
|
|
46
|
+
status: 'failed',
|
|
47
|
+
progress: { completed, total, estimatedSecondsRemaining: null },
|
|
48
|
+
failedDuring: phase,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const failedThisAttempt = batches.filter((batch) => failedDuringAttempt(batch, attemptStartedAt));
|
|
52
|
+
const remaining = total - completed - sumRecipients(failedThisAttempt);
|
|
53
|
+
const samples = completedBatches.map((batch) => ({
|
|
54
|
+
recipientCount: batch.recipientCount,
|
|
55
|
+
timestamp: (phase === 'preparing' ? batch.createdAt : batch.updatedAt).getTime(),
|
|
56
|
+
}));
|
|
57
|
+
return {
|
|
58
|
+
status: phase,
|
|
59
|
+
progress: {
|
|
60
|
+
completed,
|
|
61
|
+
total,
|
|
62
|
+
estimatedSecondsRemaining: estimateSecondsRemaining({
|
|
63
|
+
remaining,
|
|
64
|
+
samples,
|
|
65
|
+
attemptStartedAt,
|
|
66
|
+
}),
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function sumRecipients(batches) {
|
|
71
|
+
return batches.reduce((sum, batch) => sum + batch.recipientCount, 0);
|
|
72
|
+
}
|
|
73
|
+
// A batch that fails is only retried together with its email, so within an attempt it is finished work.
|
|
74
|
+
function failedDuringAttempt(batch, attemptStartedAt) {
|
|
75
|
+
return (batch.status === 'failed' &&
|
|
76
|
+
attemptStartedAt !== null &&
|
|
77
|
+
batch.updatedAt.getTime() >= attemptStartedAt);
|
|
78
|
+
}
|
|
79
|
+
function estimateSecondsRemaining({ remaining, samples, attemptStartedAt, }) {
|
|
80
|
+
if (remaining <= 0) {
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
const sorted = samples
|
|
84
|
+
.filter((sample) => sample.recipientCount > 0 &&
|
|
85
|
+
(attemptStartedAt === null || sample.timestamp >= attemptStartedAt))
|
|
86
|
+
.sort((a, b) => a.timestamp - b.timestamp);
|
|
87
|
+
// Coalesce completions sharing a timestamp so database timestamp precision does
|
|
88
|
+
// not turn their recipients into zero-duration samples or make row order matter.
|
|
89
|
+
const completions = [];
|
|
90
|
+
for (const sample of sorted) {
|
|
91
|
+
const previous = completions.at(-1);
|
|
92
|
+
if (previous?.timestamp === sample.timestamp) {
|
|
93
|
+
previous.recipientCount += sample.recipientCount;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
completions.push({ ...sample });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Limit usable timestamps, not rows: fast sends may finish many batches per timestamp.
|
|
100
|
+
const window = completions.slice(-ETA_COMPLETION_WINDOW);
|
|
101
|
+
// Three distinct timestamps provide two measured intervals for an initial rate.
|
|
102
|
+
if (window.length < ETA_MIN_INTERVALS + 1) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
// Measure combined throughput across the window, including overlapping workers.
|
|
106
|
+
// The first completion is only the baseline: its recipients were processed
|
|
107
|
+
// before the measured time span.
|
|
108
|
+
const seconds = (window[window.length - 1].timestamp - window[0].timestamp) / 1000;
|
|
109
|
+
const recipients = window.slice(1).reduce((sum, sample) => sum + sample.recipientCount, 0);
|
|
110
|
+
return Math.ceil((remaining * seconds) / recipients);
|
|
111
|
+
}
|
|
@@ -54,38 +54,6 @@ class EmailSuppressionData {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
/**
|
|
58
|
-
* @abstract
|
|
59
|
-
* @implements {IEmailSuppressionList}
|
|
60
|
-
*/
|
|
61
|
-
class AbstractEmailSuppressionList {
|
|
62
|
-
/**
|
|
63
|
-
* @param {string} email
|
|
64
|
-
* @returns {Promise<boolean>}
|
|
65
|
-
*/
|
|
66
|
-
// eslint-disable-next-line
|
|
67
|
-
async removeEmail(email) {
|
|
68
|
-
return Promise.reject();
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* @param {string} email
|
|
73
|
-
* @returns {Promise<EmailSuppressionData>}
|
|
74
|
-
*/
|
|
75
|
-
// eslint-disable-next-line
|
|
76
|
-
async getSuppressionData(email) {
|
|
77
|
-
return Promise.reject();
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* @param {string[]} emails
|
|
82
|
-
* @returns {Promise<EmailSuppressionData[]>}
|
|
83
|
-
*/
|
|
84
|
-
async getBulkSuppressionData(emails) {
|
|
85
|
-
return Promise.all(emails.map((email) => this.getSuppressionData(email)));
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
57
|
class EmailSuppressedEvent {
|
|
90
58
|
/**
|
|
91
59
|
* @readonly
|
|
@@ -120,7 +88,6 @@ class EmailSuppressedEvent {
|
|
|
120
88
|
}
|
|
121
89
|
|
|
122
90
|
module.exports = {
|
|
123
|
-
AbstractEmailSuppressionList,
|
|
124
91
|
EmailSuppressionData,
|
|
125
92
|
EmailSuppressedEvent,
|
|
126
93
|
};
|
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
const {
|
|
2
|
-
AbstractEmailSuppressionList,
|
|
3
|
-
EmailSuppressionData,
|
|
4
|
-
EmailSuppressedEvent,
|
|
5
|
-
} = require('./email-suppression-list');
|
|
1
|
+
const { EmailSuppressionData, EmailSuppressedEvent } = require('./email-suppression-list');
|
|
6
2
|
const { SpamComplaintEvent } = require('../email-service/events/spam-complaint-event');
|
|
7
3
|
const { EmailBouncedEvent } = require('../email-service/events/email-bounced-event');
|
|
8
4
|
const DomainEvents = require('@tryghost/domain-events');
|
|
9
5
|
const logging = require('@tryghost/logging');
|
|
10
6
|
const models = require('../../models');
|
|
7
|
+
/** @import {IEmailSuppressionList} from './email-suppression-list' */
|
|
11
8
|
|
|
12
9
|
/**
|
|
13
10
|
* @typedef {object} IMailgunAPIClient
|
|
@@ -16,14 +13,16 @@ const models = require('../../models');
|
|
|
16
13
|
* @prop {(email: string) => Promise<any>} removeUnsubscribe
|
|
17
14
|
*/
|
|
18
15
|
|
|
19
|
-
|
|
16
|
+
/**
|
|
17
|
+
* @implements {IEmailSuppressionList}
|
|
18
|
+
*/
|
|
19
|
+
class MailgunEmailSuppressionList {
|
|
20
20
|
/**
|
|
21
21
|
* @param {object} deps
|
|
22
22
|
* @param {import('bookshelf').Model} deps.Suppression
|
|
23
23
|
* @param {IMailgunAPIClient} deps.apiClient
|
|
24
24
|
*/
|
|
25
25
|
constructor(deps) {
|
|
26
|
-
super();
|
|
27
26
|
this.Suppression = deps.Suppression;
|
|
28
27
|
this.apiClient = deps.apiClient;
|
|
29
28
|
}
|