ghost 6.24.0 → 6.25.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/tryghost-i18n-6.25.1.tgz +0 -0
- package/components/tryghost-parse-email-address-6.25.1.tgz +0 -0
- package/core/boot.js +3 -1
- package/core/built/admin/assets/{PolarAngleAxis-7TlIQL3R.js → PolarAngleAxis-C6ZeSpZB.js} +1 -1
- package/core/built/admin/assets/{_baseAssignValue-DJsCJz6y.js → _baseAssignValue-BKAl5FrV.js} +1 -1
- package/core/built/admin/assets/{a-large-small-BnBpzYwl.js → a-large-small-B_ulQCIs.js} +1 -1
- package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +1 -1
- package/core/built/admin/assets/admin-x-settings/{code-editor-view-De3K_osj.mjs → code-editor-view-CBYT1gA1.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-bGxDzD13.mjs → index-CN5CixaU.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-5D8KqsFU.mjs → index-CPQODntW.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-BIUDn7ot.mjs → index-CgTfOd7g.mjs} +9 -9
- package/core/built/admin/assets/admin-x-settings/{modals-DuokUyAH.mjs → modals-_y-6HROx.mjs} +2 -2
- package/core/built/admin/assets/{at-sign-DC4NtDDM.js → at-sign-B8xWKM0y.js} +1 -1
- package/core/built/admin/assets/{audience-CC5U4UxI.js → audience-CBOR--z9.js} +1 -1
- package/core/built/admin/assets/{avatar-flipboard-_GQfQXQc.js → avatar-flipboard-IipGb_wZ.js} +1 -1
- package/core/built/admin/assets/{bluesky-sharing-BKyzPl9y.js → bluesky-sharing-CkI1QuAm.js} +1 -1
- package/core/built/admin/assets/{chart-BCQFlmhJ.js → chart-_slUcr-K.js} +1 -1
- package/core/built/admin/assets/{chunk.423.dae9a3bbe7845c3a3cfc.js → chunk.434.de8a15730373eccfeefe.js} +13 -101
- package/core/built/admin/assets/{chunk.524.b5e454aa74df4428420c.js → chunk.524.5b9b463506a16221fc91.js} +6 -6
- package/core/built/admin/assets/{chunk.582.20635869be68725e6d5c.js → chunk.582.d8eae1661d753befaefd.js} +7 -7
- package/core/built/admin/assets/{chunk.123.1255d9e815f933dfdc57.js → chunk.630.3dbb0231a2d871126a80.js} +8 -6
- package/core/built/admin/assets/{code-editor-view-BXnjo7qe.js → code-editor-view-BWMqr2JV.js} +1 -1
- package/core/built/admin/assets/comments-BeoA0nfK.js +1 -0
- package/core/built/admin/assets/{content-helpers-CQXipRWa.js → content-helpers-CmTGdEmL.js} +1 -1
- package/core/built/admin/assets/{copy-B-MxRWPu.js → copy-CDHcAPJw.js} +1 -1
- package/core/built/admin/assets/{data-list-B2sqn7IE.js → data-list-Crb1ffbp.js} +1 -1
- package/core/built/admin/assets/{deleted-feed-item-DQi676Dy.js → deleted-feed-item-DBW0zJQg.js} +1 -1
- package/core/built/admin/assets/{dropzone-GgOCfK2j.js → dropzone-B9JUsQlW.js} +3 -3
- package/core/built/admin/assets/{edit-profile-COx5FmF9.js → edit-profile-Dz-IhetN.js} +1 -1
- package/core/built/admin/assets/{empty-indicator-Bf4Dqk72.js → empty-indicator-2QGnEbtY.js} +1 -1
- package/core/built/admin/assets/{en-BkI47Ou-.js → en-BRQS1Z8j.js} +1 -1
- package/core/built/admin/assets/{feed-Dzc-8jwd.js → feed-T3kW9UJA.js} +1 -1
- package/core/built/admin/assets/filters-ChOMQQZW.js +1 -0
- package/core/built/admin/assets/{gh-chart-Ch6dYUac.js → gh-chart-fuWw60Ka.js} +1 -1
- package/core/built/admin/assets/{ghost-69e448feab2cb78aba64f7177fa4d6f0.js → ghost-78b4a517ac907213ab47708eef33bc6f.js} +423 -542
- package/core/built/admin/assets/{growth-BIBJdcm6.js → growth-D9sCnmon.js} +1 -1
- package/core/built/admin/assets/{hash-BGd7HEsT.js → hash-CI1pQ1UC.js} +1 -1
- package/core/built/admin/assets/header-OevT95FI.js +1 -0
- package/core/built/admin/assets/{inbox-CFzKvW3h.js → inbox-DYY8-M1K.js} +1 -1
- package/core/built/admin/assets/{index-Dy8dCc8t.js → index-8CZdLQzu.js} +4 -4
- package/core/built/admin/assets/{index-DIO8ceib.js → index-BDAOs7pI.js} +1 -1
- package/core/built/admin/assets/{index-BZyG96u1.js → index-BEEgMX-_.js} +1 -1
- package/core/built/admin/assets/index-BbveDn8H.css +1 -0
- package/core/built/admin/assets/{index-BYXKsmpv.js → index-C-Cpbee5.js} +1 -1
- package/core/built/admin/assets/{index-DDJAHYHQ.js → index-CCF2RKxR.js} +12 -12
- package/core/built/admin/assets/{index-ZuWSQ3L6.js → index-CEnfjH5h.js} +93 -93
- package/core/built/admin/assets/{index-_WMp37yL.js → index-CcFVbffG.js} +1 -1
- package/core/built/admin/assets/{index-DpZazR71.js → index-CyMlhfZJ.js} +1 -1
- package/core/built/admin/assets/{index-DM98UB_U.js → index-DBZYUfjO.js} +1 -1
- package/core/built/admin/assets/{index-D0gRP4Gp.js → index-DJa3pLmx.js} +1 -1
- package/core/built/admin/assets/{index-n5opb8tL.js → index-DVqmhe-L.js} +1 -1
- package/core/built/admin/assets/{index-S2DGJkmz.js → index-DW5aEj6O.js} +2 -2
- package/core/built/admin/assets/{index-CvX5YYEz.js → index-DcFgRUSN.js} +1 -1
- package/core/built/admin/assets/{index-7NTHsKgE.js → index-DjgsxHeB.js} +1 -1
- package/core/built/admin/assets/index-mqQc8MVd.js +1 -0
- package/core/built/admin/assets/{index-CsHjDdYB.js → index-qQZsxgFE.js} +1 -1
- package/core/built/admin/assets/{koenig-lexical-8qAZ5o1x.js → koenig-lexical-CsbE6fpi.js} +1 -1
- package/core/built/admin/assets/kpis-Dw04ruBN.js +1 -0
- package/core/built/admin/assets/{label-CofMdj8x.js → label-tBvn89sm.js} +1 -1
- package/core/built/admin/assets/{links-MSlYSad1.js → links-Do4jcjIh.js} +1 -1
- package/core/built/admin/assets/{loader-circle-CF5G2g_Z.js → loader-circle-C8exRUku.js} +4 -4
- package/core/built/admin/assets/{lucide-react-DZiAu6PQ.js → lucide-react-4VO5Dwow.js} +1 -1
- package/core/built/admin/assets/{main-layout-DEVUkqt_.js → main-layout-BAqXRgAx.js} +1 -1
- package/core/built/admin/assets/{member-route-mLKS-PsA.js → member-route-D4n-RbQd.js} +1 -1
- package/core/built/admin/assets/members-DRLgwCgJ.js +15 -0
- package/core/built/admin/assets/{message-square-text-DESy3kSk.js → message-square-text-CC_1bY-Q.js} +5 -5
- package/core/built/admin/assets/{minus-DVNwSjD6.js → minus-DSyx9DCe.js} +1 -1
- package/core/built/admin/assets/{modals-FYjLOMaQ.js → modals-AryZr_DA.js} +13 -13
- package/core/built/admin/assets/{moderation-u4WS46a-.js → moderation-DwKm9LO-.js} +1 -1
- package/core/built/admin/assets/{newsletter-Cj0UCg4J.js → newsletter-C0rydPGS.js} +1 -1
- package/core/built/admin/assets/{newsletters-_UvhdBE3.js → newsletters-DKdmxfDG.js} +1 -1
- package/core/built/admin/assets/{note-CRnFryTp.js → note-996Zos9G.js} +1 -1
- package/core/built/admin/assets/{overview-Cr0dGxEU.js → overview-r17hlWAB.js} +1 -1
- package/core/built/admin/assets/{pagemenu-BqoDENpI.js → pagemenu-DzpB1AkZ.js} +17 -17
- package/core/built/admin/assets/{post-analytics-DIr_rDtL.js → post-analytics-AwufPVFE.js} +1 -1
- package/core/built/admin/assets/{post-analytics-context-WA--pwK5.js → post-analytics-context-GFe0lMc-.js} +1 -1
- package/core/built/admin/assets/{post-analytics-header-D97SoEaV.js → post-analytics-header-DmGyNvP9.js} +1 -1
- package/core/built/admin/assets/{post-share-modal-DBxR7WCX.js → post-share-modal-CTpdCS2X.js} +1 -1
- package/core/built/admin/assets/posts/{comments-Bswx2dzs.mjs → comments-Cvt31uBs.mjs} +597 -674
- package/core/built/admin/assets/posts/{dialog-67vvNv8e.mjs → dialog-DaViJlnd.mjs} +3 -3
- package/core/built/admin/assets/posts/{dropdown-menu-BKZfsAPJ.mjs → dropdown-menu-B4KGMiRI.mjs} +5 -5
- package/core/built/admin/assets/posts/{empty-indicator-DTmBQa_3.mjs → empty-indicator-BaD6pCXM.mjs} +2 -2
- package/core/built/admin/assets/posts/filters-BsCI4RJg.mjs +2835 -0
- package/core/built/admin/assets/posts/{get-site-timezone-D75cHSJ1.mjs → get-site-timezone-Bo-iI2bI.mjs} +2 -2
- package/core/built/admin/assets/posts/{growth-BqJWjpAp.mjs → growth-BJxIDp2o.mjs} +14 -14
- package/core/built/admin/assets/posts/header-7zhnErbb.mjs +100 -0
- package/core/built/admin/assets/posts/{heading-DXxLDQ96.mjs → heading-BCDx_eVF.mjs} +2 -2
- package/core/built/admin/assets/posts/{hooks-pVotzebr.mjs → hooks-DQL2O_El.mjs} +2 -2
- package/core/built/admin/assets/posts/{index-DZ1HzbGS.mjs → index-0mEgtljt.mjs} +3 -3
- package/core/built/admin/assets/posts/{index-By5yPnMZ.mjs → index-BTzJVQ6S.mjs} +10 -10
- package/core/built/admin/assets/posts/{input-DdXe2Fjm.mjs → input-DaHXv92f.mjs} +2 -2
- package/core/built/admin/assets/posts/{kpis-1MXZeSC9.mjs → kpis-GRsx6fPV.mjs} +10 -10
- package/core/built/admin/assets/posts/{links-DGzzdzsH.mjs → links-B1q_pqE9.mjs} +4 -4
- package/core/built/admin/assets/posts/{loading-indicator-CSj2spwS.mjs → loading-indicator-DiYqXomj.mjs} +3 -3
- package/core/built/admin/assets/posts/{main-layout-CGs25JVH.mjs → main-layout-DeqPTJTe.mjs} +2 -2
- package/core/built/admin/assets/posts/{members-BRcED5fL.mjs → members-LABk5yI5.mjs} +4102 -3442
- package/core/built/admin/assets/posts/{newsletter-D706LlgM.mjs → newsletter-C_nbHBaP.mjs} +15 -15
- package/core/built/admin/assets/posts/{overview-5dG5ypjx.mjs → overview-AJnKv4Ze.mjs} +16 -16
- package/core/built/admin/assets/posts/{post-analytics-wvNuOa4c.mjs → post-analytics-b_CKsePp.mjs} +6 -6
- package/core/built/admin/assets/posts/{post-analytics-context-BqnFe05q.mjs → post-analytics-context-D6jtpxxm.mjs} +5 -5
- package/core/built/admin/assets/posts/{post-analytics-header-Dsi5l6hb.mjs → post-analytics-header-R5UF_tF0.mjs} +11 -11
- package/core/built/admin/assets/posts/{post-share-modal-DWe35McY.mjs → post-share-modal-y4BEOajk.mjs} +4 -4
- package/core/built/admin/assets/posts/posts-C2hNrf1c.mjs +37 -0
- package/core/built/admin/assets/posts/posts.js +1 -1
- package/core/built/admin/assets/posts/{reply-B4vwBMwj.mjs → reply-2Uvi3Kxb.mjs} +2 -2
- package/core/built/admin/assets/posts/{search-DQuB1WMm.mjs → search-se7meOnL.mjs} +2 -2
- package/core/built/admin/assets/posts/{select-BMPHyOdQ.mjs → select-DpUpauFg.mjs} +8 -8
- package/core/built/admin/assets/posts/{separator-DgZ2VF9r.mjs → separator-CdBM46dw.mjs} +3 -3
- package/core/built/admin/assets/posts/{settings-D-wnf7Qg.mjs → settings-Chhj9xKP.mjs} +2 -2
- package/core/built/admin/assets/posts/{sheet-SOdCzxUt.mjs → sheet--k3SiDko.mjs} +3 -3
- package/core/built/admin/assets/posts/{skeleton-Du7JXZ_1.mjs → skeleton-CUSLSLoY.mjs} +3 -3
- package/core/built/admin/assets/posts/{source-icon-DcBtKbCl.mjs → source-icon-BjYUIoXN.mjs} +3 -3
- package/core/built/admin/assets/posts/{stats-CeLav-Hu.mjs → stats-BRdXKaxM.mjs} +4 -4
- package/core/built/admin/assets/posts/{table-DlaJMBps.mjs → table-Bgi_UcfL.mjs} +2 -2
- package/core/built/admin/assets/posts/{tabs-BQQbtw2g.mjs → tabs-DT8Bh72E.mjs} +14 -14
- package/core/built/admin/assets/posts/tags-BCFLnZYl.mjs +399 -0
- package/core/built/admin/assets/posts/{tags-CLyaNf39.mjs → tags-Kv2q0o3p.mjs} +2 -2
- package/core/built/admin/assets/posts/use-scroll-restoration-CLAUAal3.mjs +517 -0
- package/core/built/admin/assets/posts/{use-infinite-virtual-scroll-B_NwiBXK.mjs → virtual-list-window-CQDEqNBL.mjs} +246 -262
- package/core/built/admin/assets/posts/{web-TEVKbXSI.mjs → web-CqN0MQU6.mjs} +18 -18
- package/core/built/admin/assets/posts-GvXDuhVG.js +1 -0
- package/core/built/admin/assets/{referrers-J4vexZCf.js → referrers-Bd9pW-WX.js} +1 -1
- package/core/built/admin/assets/{repeat-BxOK9zhN.js → repeat-y6HlMgGO.js} +2 -2
- package/core/built/admin/assets/{reply-BoTN-eE0.js → reply-Cc8ryI5v.js} +2 -2
- package/core/built/admin/assets/{select-Bd1zcqox.js → select-CGr_5qad.js} +1 -1
- package/core/built/admin/assets/{settings-D72z6T_a.js → settings-BPsfSgg3.js} +1 -1
- package/core/built/admin/assets/{settings-BGbGoa4s.js → settings-BTd0Adha.js} +19 -19
- package/core/built/admin/assets/{sort-button-BOLTpImz.js → sort-button-k76_5HR2.js} +1 -1
- package/core/built/admin/assets/{source-icon-BqEM5MvU.js → source-icon-DMt0UskC.js} +1 -1
- package/core/built/admin/assets/{sprout-Bf-wc2Vo.js → sprout-Td9eaOoY.js} +2 -2
- package/core/built/admin/assets/{square-Cj_bJ0HZ.js → square-Df8vEYlG.js} +1 -1
- package/core/built/admin/assets/stats/{audience-BiHYBUzT.mjs → audience-DMTObpL8.mjs} +3 -3
- package/core/built/admin/assets/stats/{content-helpers-CIXZiyCm.mjs → content-helpers-ReMp9KJN.mjs} +4 -4
- package/core/built/admin/assets/stats/{index-CkKk-p14.mjs → index-6L-yLKFX.mjs} +8 -8
- package/core/built/admin/assets/stats/{index-DBB8Oih1.mjs → index-CIUyjeP-.mjs} +5 -5
- package/core/built/admin/assets/stats/{index-9zQR4gr0.mjs → index-CMSDpQm3.mjs} +3626 -3513
- package/core/built/admin/assets/stats/{index-DErXBfDM.mjs → index-Ca-_KJzy.mjs} +5 -5
- package/core/built/admin/assets/stats/{index-DMSmQLxG.mjs → index-CfO5yPSP.mjs} +5 -5
- package/core/built/admin/assets/stats/{sort-button-BqY7GFaO.mjs → sort-button-BB_M2CdZ.mjs} +3 -3
- package/core/built/admin/assets/stats/{stats-BeJgKX2G.mjs → stats-B2nnuLeU.mjs} +4 -4
- package/core/built/admin/assets/stats/stats.js +1 -1
- package/core/built/admin/assets/stats/{tabs-CPz357mW.mjs → tabs-DjJG-d3O.mjs} +3 -3
- package/core/built/admin/assets/stats/{use-growth-stats-WMYXBQE3.mjs → use-growth-stats-BMT5M7l6.mjs} +3 -3
- package/core/built/admin/assets/{stats-DWpHtRym.js → stats-Bs9ZbmXX.js} +1 -1
- package/core/built/admin/assets/stats-view-BWoPuaLz.js +1 -0
- package/core/built/admin/assets/{step-1-Bjya9Gt0.js → step-1-BUvsuCs0.js} +1 -1
- package/core/built/admin/assets/{step-2-CJ6I8JNc.js → step-2-DI9ekXls.js} +1 -1
- package/core/built/admin/assets/{step-3-3fs6Y2Kj.js → step-3-DzT_XhaS.js} +2 -2
- package/core/built/admin/assets/{table-B4zlzEDP.js → table-BKqaEGAm.js} +1 -1
- package/core/built/admin/assets/{tabs-mR5oJGm5.js → tabs-G6Ql8vbf.js} +1 -1
- package/core/built/admin/assets/tags-BHm-dlBs.js +1 -0
- package/core/built/admin/assets/{tags-CsgMtkFL.js → tags-_taMP0sJ.js} +1 -1
- package/core/built/admin/assets/{textarea-CQjEvapR.js → textarea-DcVuGYYq.js} +1 -1
- package/core/built/admin/assets/{tiers-CQE0v797.js → tiers-kv-xBoqQ.js} +1 -1
- package/core/built/admin/assets/{toggle-group-CR-Z-bJL.js → toggle-group-DsatLdt7.js} +1 -1
- package/core/built/admin/assets/{topic-filter-_NyD55eH.js → topic-filter-DkJyIuAf.js} +1 -1
- package/core/built/admin/assets/{trash-BFSLkv0z.js → trash-BeZZgbn-.js} +1 -1
- package/core/built/admin/assets/{upload-BTxT4v2z.js → upload-BG7KNIn7.js} +4 -4
- package/core/built/admin/assets/{use-growth-stats-BGymNu5O.js → use-growth-stats-CNGdl44N.js} +1 -1
- package/core/built/admin/assets/use-scroll-restoration-CQ3w0W98.js +1 -0
- package/core/built/admin/assets/{use-simple-pagination-Cq7Afp_c.js → use-simple-pagination-BeIJ_5Vv.js} +1 -1
- package/core/built/admin/assets/{user-plus-DMl69yKl.js → user-plus-CMxR50-Y.js} +1 -1
- package/core/built/admin/assets/{user-round-check-CVuZXUPz.js → user-round-check-2nhPmqP3.js} +1 -1
- package/core/built/admin/assets/virtual-list-window-Uq8oKYVG.js +4 -0
- package/core/built/admin/assets/{wallet-cards-zHR_y6Sp.js → wallet-cards-CJcVzH0D.js} +1 -1
- package/core/built/admin/assets/{web-CmFHgeQw.js → web-BPwTFKrc.js} +1 -1
- package/core/built/admin/index.html +7 -7
- package/core/frontend/helpers/ghost_head.js +1 -4
- package/core/server/data/migrations/versions/6.25/2026-03-26-15-47-00-insert-default-email-design-settings-row.js +42 -0
- package/core/server/data/migrations/versions/6.25/2026-03-30-20-22-25-add-email-design-setting-permissions.js +44 -0
- package/core/server/data/migrations/versions/6.25/2026-03-30-22-16-43-add-email-design-setting-id-to-automated-emails.js +8 -0
- package/core/server/data/migrations/versions/6.25/2026-03-31-13-12-10-backfill-automated-emails-email-design-setting-id.js +45 -0
- package/core/server/data/migrations/versions/6.25/2026-03-31-20-31-19-drop-nullable-on-automated-emails-email-design-setting-id.js +3 -0
- package/core/server/data/schema/fixtures/fixtures.json +35 -0
- package/core/server/data/schema/schema.js +1 -0
- package/core/server/data/seeders/importers/comment-reports-importer.js +1 -3
- package/core/server/data/seeders/importers/comments-importer.js +3 -2
- package/core/server/data/seeders/importers/email-batches-importer.js +3 -3
- package/core/server/data/seeders/importers/email-recipients-importer.js +6 -6
- package/core/server/data/seeders/importers/emails-importer.js +4 -3
- package/core/server/data/seeders/importers/members-click-events-importer.js +1 -1
- package/core/server/data/seeders/importers/members-created-events-importer.js +2 -1
- package/core/server/data/seeders/importers/members-feedback-importer.js +3 -4
- package/core/server/data/seeders/importers/members-login-events-importer.js +3 -2
- package/core/server/data/seeders/importers/members-status-events-importer.js +1 -2
- package/core/server/data/seeders/importers/members-stripe-customers-importer.js +2 -1
- package/core/server/data/seeders/importers/members-stripe-customers-subscriptions-importer.js +4 -2
- package/core/server/data/seeders/importers/members-subscribe-events-importer.js +1 -2
- package/core/server/data/seeders/importers/members-subscription-created-events-importer.js +3 -1
- package/core/server/data/seeders/importers/offer-redemptions-importer.js +3 -3
- package/core/server/data/seeders/utils/database-date.js +26 -1
- package/core/server/models/automated-email.js +27 -0
- package/core/server/models/email-design-setting.js +27 -0
- package/core/server/services/members/service.js +0 -22
- package/core/server/services/stripe/service.js +1 -2
- package/core/server/services/verification/verification-webhook-service.js +2 -2
- package/core/server/services/verification/verification-webhook-service.ts +2 -2
- package/core/server/services/verification-trigger.js +34 -69
- package/core/server/services/webhooks/webhook-trigger.js +1 -1
- package/core/shared/labs.js +2 -2
- package/package.json +10 -28
- package/tsconfig.tsbuildinfo +1 -1
- package/yarn.lock +423 -107
- package/components/tryghost-i18n-6.24.0.tgz +0 -0
- package/components/tryghost-parse-email-address-6.24.0.tgz +0 -0
- package/core/built/admin/assets/comments-CteqV7ZN.js +0 -1
- package/core/built/admin/assets/filters-DXXaCya7.js +0 -1
- package/core/built/admin/assets/index-QB-va_sk.css +0 -1
- package/core/built/admin/assets/index-nBUk6L1X.js +0 -1
- package/core/built/admin/assets/kpis-Cv75oSPY.js +0 -1
- package/core/built/admin/assets/members-DcElgZ2t.js +0 -15
- package/core/built/admin/assets/posts/filters-Ds_ZaYIU.mjs +0 -2722
- package/core/built/admin/assets/posts/posts-DI5YQFp4.mjs +0 -17
- package/core/built/admin/assets/posts/tags-CjwolIiT.mjs +0 -395
- package/core/built/admin/assets/posts/use-scroll-restoration-YSR5jwQl.mjs +0 -232
- package/core/built/admin/assets/posts-CJHEP4C-.js +0 -1
- package/core/built/admin/assets/stats-view-CcyaLPwj.js +0 -1
- package/core/built/admin/assets/tags-DEFC6GpD.js +0 -1
- package/core/built/admin/assets/use-infinite-virtual-scroll-CNqGwgHd.js +0 -4
- package/core/built/admin/assets/use-scroll-restoration-BWtb0NrS.js +0 -1
- package/core/shared/config/env/config.testing-browser.json +0 -98
- /package/core/built/admin/assets/{chunk.423.dae9a3bbe7845c3a3cfc.js.LICENSE.txt → chunk.434.de8a15730373eccfeefe.js.LICENSE.txt} +0 -0
|
@@ -50,7 +50,7 @@ class MembersClickEventsImporter extends TableImporter {
|
|
|
50
50
|
}
|
|
51
51
|
this.amount -= 1;
|
|
52
52
|
|
|
53
|
-
const openedAt =
|
|
53
|
+
const openedAt = dateToDatabaseString.parse(this.model.opened_at);
|
|
54
54
|
const laterOn = new Date(openedAt.getTime() + 1000 * 60 * 15);
|
|
55
55
|
const clickTime = faker.date.between(openedAt.getTime(), laterOn.getTime()); //added getTime here because it threw random errors
|
|
56
56
|
|
|
@@ -47,7 +47,8 @@ class MembersCreatedEventsImporter extends TableImporter {
|
|
|
47
47
|
};
|
|
48
48
|
|
|
49
49
|
if (source === 'member' && luck(30)) {
|
|
50
|
-
const
|
|
50
|
+
const memberCreatedAt = dateToDatabaseString.parse(this.model.created_at);
|
|
51
|
+
const post = this.posts.find(p => p.visibility === 'public' && dateToDatabaseString.parse(p.published_at) < memberCreatedAt);
|
|
51
52
|
if (post) {
|
|
52
53
|
attribution = {
|
|
53
54
|
attribution_id: post.id,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const TableImporter = require('./table-importer');
|
|
2
|
-
const {faker} = require('@faker-js/faker');
|
|
3
2
|
const {luck} = require('../utils/random');
|
|
4
3
|
const dateToDatabaseString = require('../utils/database-date');
|
|
5
4
|
|
|
@@ -25,10 +24,10 @@ class MembersFeedbackImporter extends TableImporter {
|
|
|
25
24
|
return null;
|
|
26
25
|
}
|
|
27
26
|
|
|
28
|
-
const openedAt =
|
|
29
|
-
const laterOn =
|
|
27
|
+
const openedAt = dateToDatabaseString.parse(this.model.opened_at);
|
|
28
|
+
const laterOn = dateToDatabaseString.parse(this.model.opened_at);
|
|
30
29
|
laterOn.setMinutes(laterOn.getMinutes() + 60);
|
|
31
|
-
const feedbackTime =
|
|
30
|
+
const feedbackTime = dateToDatabaseString.randomBetween(openedAt, laterOn);
|
|
32
31
|
|
|
33
32
|
const postId = this.emails.find(email => email.id === this.model.email_id).post_id;
|
|
34
33
|
return {
|
|
@@ -35,9 +35,10 @@ class MembersLoginEventsImporter extends TableImporter {
|
|
|
35
35
|
|
|
36
36
|
setReferencedModel(model) {
|
|
37
37
|
this.model = model;
|
|
38
|
+
const memberCreatedAt = dateToDatabaseString.parse(model.created_at);
|
|
38
39
|
|
|
39
40
|
const endDate = new Date();
|
|
40
|
-
const daysBetween = Math.ceil((endDate.valueOf() -
|
|
41
|
+
const daysBetween = Math.ceil((endDate.valueOf() - memberCreatedAt.valueOf()) / (1000 * 60 * 60 * 24));
|
|
41
42
|
|
|
42
43
|
// Assuming most people either subscribe and lose interest, or maintain steady readership
|
|
43
44
|
const shape = luck(40) ? 'ease-out' : 'flat';
|
|
@@ -47,7 +48,7 @@ class MembersLoginEventsImporter extends TableImporter {
|
|
|
47
48
|
// Steady readers login more, readers who lose interest read less overall.
|
|
48
49
|
// ceil because members will all have logged in at least once
|
|
49
50
|
total: Math.min(5, shape === 'flat' ? Math.ceil(daysBetween / 3) : Math.ceil(daysBetween / 7)),
|
|
50
|
-
startTime:
|
|
51
|
+
startTime: memberCreatedAt,
|
|
51
52
|
endTime: endDate
|
|
52
53
|
});
|
|
53
54
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const TableImporter = require('./table-importer');
|
|
2
|
-
const {faker} = require('@faker-js/faker');
|
|
3
2
|
const dateToDatabaseString = require('../utils/database-date');
|
|
4
3
|
|
|
5
4
|
class MembersStatusEventsImporter extends TableImporter {
|
|
@@ -41,7 +40,7 @@ class MembersStatusEventsImporter extends TableImporter {
|
|
|
41
40
|
member_id: model.id,
|
|
42
41
|
from_status: 'free',
|
|
43
42
|
to_status: model.status,
|
|
44
|
-
created_at: dateToDatabaseString(
|
|
43
|
+
created_at: dateToDatabaseString(dateToDatabaseString.randomBetween(model.created_at, new Date()))
|
|
45
44
|
});
|
|
46
45
|
}
|
|
47
46
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const {faker} = require('@faker-js/faker');
|
|
2
2
|
const TableImporter = require('./table-importer');
|
|
3
|
+
const dateToDatabaseString = require('../utils/database-date');
|
|
3
4
|
|
|
4
5
|
class MembersStripeCustomersImporter extends TableImporter {
|
|
5
6
|
static table = 'members_stripe_customers';
|
|
@@ -35,7 +36,7 @@ class MembersStripeCustomersImporter extends TableImporter {
|
|
|
35
36
|
// Only 30% of free members should have a stripe customer = have had a subscription in the past or tried to subscribe
|
|
36
37
|
// The number should increase the older the member is
|
|
37
38
|
|
|
38
|
-
const daysSinceMemberCreated = Math.floor((new Date() -
|
|
39
|
+
const daysSinceMemberCreated = Math.floor((new Date() - dateToDatabaseString.parse(this.model.created_at)) / (1000 * 60 * 60 * 24));
|
|
39
40
|
const shouldHaveStripeCustomer = faker.datatype.number({min: 0, max: 100}) < Math.max(Math.min(daysSinceMemberCreated / 60, 15), 2);
|
|
40
41
|
|
|
41
42
|
if (!shouldHaveStripeCustomer) {
|
package/core/server/data/seeders/importers/members-stripe-customers-subscriptions-importer.js
CHANGED
|
@@ -30,6 +30,7 @@ class MembersStripeCustomersSubscriptionsImporter extends TableImporter {
|
|
|
30
30
|
this.members = await this.transaction.select('id', 'status', 'created_at').from('members').whereIn('id', membersStripeCustomers.map(m => m.member_id));
|
|
31
31
|
|
|
32
32
|
if (this.members.length === 0) {
|
|
33
|
+
offset += limit;
|
|
33
34
|
continue;
|
|
34
35
|
}
|
|
35
36
|
|
|
@@ -95,6 +96,7 @@ class MembersStripeCustomersSubscriptionsImporter extends TableImporter {
|
|
|
95
96
|
(isMonthly ? price.interval === 'month' : price.interval === 'year');
|
|
96
97
|
});
|
|
97
98
|
const mrr = createValid ? (isMonthly ? stripePrice.amount : Math.floor(stripePrice.amount / 12)) : 0;
|
|
99
|
+
const memberCreatedAt = dateToDatabaseString.parse(member.created_at);
|
|
98
100
|
|
|
99
101
|
const referenceEndDate = this.lastSubscriptionStart ?? new Date();
|
|
100
102
|
|
|
@@ -106,7 +108,7 @@ class MembersStripeCustomersSubscriptionsImporter extends TableImporter {
|
|
|
106
108
|
}
|
|
107
109
|
}
|
|
108
110
|
|
|
109
|
-
if (referenceEndDate <
|
|
111
|
+
if (referenceEndDate < memberCreatedAt) {
|
|
110
112
|
// Not possible to create an invalid subscription here
|
|
111
113
|
return;
|
|
112
114
|
}
|
|
@@ -114,7 +116,7 @@ class MembersStripeCustomersSubscriptionsImporter extends TableImporter {
|
|
|
114
116
|
const [startDate] = generateEvents({
|
|
115
117
|
total: 1,
|
|
116
118
|
trend: 'negative',
|
|
117
|
-
startTime:
|
|
119
|
+
startTime: memberCreatedAt,
|
|
118
120
|
endTime: referenceEndDate,
|
|
119
121
|
shape: 'ease-out'
|
|
120
122
|
});
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const TableImporter = require('./table-importer');
|
|
2
|
-
const {faker} = require('@faker-js/faker');
|
|
3
2
|
const {luck} = require('../utils/random');
|
|
4
3
|
const dateToDatabaseString = require('../utils/database-date');
|
|
5
4
|
|
|
@@ -52,7 +51,7 @@ class MembersSubscribeEventsImporter extends TableImporter {
|
|
|
52
51
|
return null;
|
|
53
52
|
}
|
|
54
53
|
|
|
55
|
-
const createdAt = dateToDatabaseString(
|
|
54
|
+
const createdAt = dateToDatabaseString(dateToDatabaseString.randomBetween(this.model.created_at, new Date()));
|
|
56
55
|
const newsletterId = this.newsletters[count % this.newsletters.length].id;
|
|
57
56
|
|
|
58
57
|
return {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const TableImporter = require('./table-importer');
|
|
2
2
|
const {faker} = require('@faker-js/faker');
|
|
3
3
|
const {luck} = require('../utils/random');
|
|
4
|
+
const dateToDatabaseString = require('../utils/database-date');
|
|
4
5
|
|
|
5
6
|
class MembersSubscriptionCreatedEventsImporter extends TableImporter {
|
|
6
7
|
static table = 'members_subscription_created_events';
|
|
@@ -48,7 +49,8 @@ class MembersSubscriptionCreatedEventsImporter extends TableImporter {
|
|
|
48
49
|
};
|
|
49
50
|
|
|
50
51
|
if (luck(30)) {
|
|
51
|
-
const
|
|
52
|
+
const createdAt = dateToDatabaseString.parse(this.model.created_at);
|
|
53
|
+
const post = this.posts.find(p => p.visibility === 'public' && dateToDatabaseString.parse(p.published_at) < createdAt);
|
|
52
54
|
if (post) {
|
|
53
55
|
attribution = {
|
|
54
56
|
attribution_id: post.id,
|
|
@@ -53,7 +53,7 @@ class OfferRedemptionsImporter extends TableImporter {
|
|
|
53
53
|
this.subscriptionPool.push({
|
|
54
54
|
memberId,
|
|
55
55
|
subscriptionId: subscription.id,
|
|
56
|
-
subscriptionCreatedAt:
|
|
56
|
+
subscriptionCreatedAt: dateToDatabaseString.parse(subscription.created_at),
|
|
57
57
|
redemptionEndAt: this.getRedemptionEndDate(subscription.current_period_end),
|
|
58
58
|
availableOffers: [...matchingOffers],
|
|
59
59
|
lastRedeemedAt: null
|
|
@@ -78,7 +78,7 @@ class OfferRedemptionsImporter extends TableImporter {
|
|
|
78
78
|
|
|
79
79
|
getRedemptionEndDate(currentPeriodEnd) {
|
|
80
80
|
const now = new Date();
|
|
81
|
-
const endDate = currentPeriodEnd ?
|
|
81
|
+
const endDate = currentPeriodEnd ? dateToDatabaseString.parse(currentPeriodEnd) : now;
|
|
82
82
|
|
|
83
83
|
return endDate > now ? now : endDate;
|
|
84
84
|
}
|
|
@@ -86,7 +86,7 @@ class OfferRedemptionsImporter extends TableImporter {
|
|
|
86
86
|
getCreatedAt(subscriptionState, offer) {
|
|
87
87
|
const candidateEarliest = new Date(Math.max(
|
|
88
88
|
subscriptionState.subscriptionCreatedAt.valueOf(),
|
|
89
|
-
|
|
89
|
+
dateToDatabaseString.parse(offer.created_at).valueOf(),
|
|
90
90
|
subscriptionState.lastRedeemedAt ? subscriptionState.lastRedeemedAt.valueOf() + 1000 : 0
|
|
91
91
|
));
|
|
92
92
|
const earliest = new Date(Math.min(
|
|
@@ -1,7 +1,32 @@
|
|
|
1
|
-
|
|
1
|
+
const {faker} = require('@faker-js/faker');
|
|
2
|
+
|
|
3
|
+
const databaseDatePattern = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?$/;
|
|
4
|
+
|
|
5
|
+
function dateToDatabaseString(date) {
|
|
2
6
|
if (typeof date === 'string') {
|
|
3
7
|
// SQLite fix when reusing other dates from the db
|
|
4
8
|
return date;
|
|
5
9
|
}
|
|
6
10
|
return date.toISOString().replace('Z','').replace('T', ' ');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
dateToDatabaseString.parse = function parseDatabaseDate(date) {
|
|
14
|
+
if (date instanceof Date) {
|
|
15
|
+
return new Date(date);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (typeof date === 'string' && databaseDatePattern.test(date)) {
|
|
19
|
+
return new Date(date.replace(' ', 'T') + 'Z');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return new Date(date);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
dateToDatabaseString.randomBetween = function randomBetween(start, end) {
|
|
26
|
+
const earliest = dateToDatabaseString.parse(start);
|
|
27
|
+
const latest = dateToDatabaseString.parse(end);
|
|
28
|
+
|
|
29
|
+
return latest > earliest ? faker.date.between(earliest, latest) : earliest;
|
|
7
30
|
};
|
|
31
|
+
|
|
32
|
+
module.exports = dateToDatabaseString;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
const ghostBookshelf = require('./base');
|
|
2
|
+
const errors = require('@tryghost/errors');
|
|
2
3
|
const logging = require('@tryghost/logging');
|
|
3
4
|
const urlUtils = require('../../shared/url-utils');
|
|
4
5
|
const lexicalLib = require('../lib/lexical');
|
|
5
6
|
const {MEMBER_WELCOME_EMAIL_SLUGS} = require('../services/member-welcome-emails/constants');
|
|
6
7
|
|
|
7
8
|
const MEMBER_WELCOME_EMAIL_SLUG_SET = new Set(Object.values(MEMBER_WELCOME_EMAIL_SLUGS));
|
|
9
|
+
const DEFAULT_EMAIL_DESIGN_SETTING_SLUG = 'default-automated-email';
|
|
8
10
|
|
|
9
11
|
const AutomatedEmail = ghostBookshelf.Model.extend({
|
|
10
12
|
tableName: 'automated_emails',
|
|
@@ -15,6 +17,13 @@ const AutomatedEmail = ghostBookshelf.Model.extend({
|
|
|
15
17
|
};
|
|
16
18
|
},
|
|
17
19
|
|
|
20
|
+
/**
|
|
21
|
+
* @returns {import('bookshelf').Model}
|
|
22
|
+
*/
|
|
23
|
+
emailDesignSetting() {
|
|
24
|
+
return this.belongsTo('EmailDesignSetting', 'email_design_setting_id', 'id');
|
|
25
|
+
},
|
|
26
|
+
|
|
18
27
|
parse() {
|
|
19
28
|
const attrs = ghostBookshelf.Model.prototype.parse.apply(this, arguments);
|
|
20
29
|
|
|
@@ -26,6 +35,24 @@ const AutomatedEmail = ghostBookshelf.Model.extend({
|
|
|
26
35
|
return attrs;
|
|
27
36
|
},
|
|
28
37
|
|
|
38
|
+
async onCreating(model, attrs, options) {
|
|
39
|
+
if (!model.get('email_design_setting_id')) {
|
|
40
|
+
const emailDesignSetting = await ghostBookshelf.model('EmailDesignSetting').findOne({
|
|
41
|
+
slug: DEFAULT_EMAIL_DESIGN_SETTING_SLUG
|
|
42
|
+
}, options);
|
|
43
|
+
|
|
44
|
+
if (!emailDesignSetting) {
|
|
45
|
+
throw new errors.InternalServerError({
|
|
46
|
+
message: 'Missing default email design setting for automated emails'
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
model.set('email_design_setting_id', emailDesignSetting.get('id'));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return ghostBookshelf.Model.prototype.onCreating.call(this, model, attrs, options);
|
|
54
|
+
},
|
|
55
|
+
|
|
29
56
|
// Alternative to Bookshelf's .format() that is only called when writing to db
|
|
30
57
|
formatOnWrite(attrs) {
|
|
31
58
|
// Ensure lexical URLs are stored as transform-ready with __GHOST_URL__ representing config.url
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const ghostBookshelf = require('./base');
|
|
2
|
+
|
|
3
|
+
const EmailDesignSetting = ghostBookshelf.Model.extend({
|
|
4
|
+
tableName: 'email_design_settings',
|
|
5
|
+
|
|
6
|
+
defaults() {
|
|
7
|
+
return {
|
|
8
|
+
background_color: 'light',
|
|
9
|
+
header_background_color: 'transparent',
|
|
10
|
+
show_header_title: true,
|
|
11
|
+
button_color: 'accent',
|
|
12
|
+
button_corners: 'rounded',
|
|
13
|
+
button_style: 'fill',
|
|
14
|
+
link_color: 'accent',
|
|
15
|
+
link_style: 'underline',
|
|
16
|
+
body_font_category: 'sans_serif',
|
|
17
|
+
title_font_category: 'sans_serif',
|
|
18
|
+
title_font_weight: 'bold',
|
|
19
|
+
image_corners: 'square',
|
|
20
|
+
show_badge: true
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
module.exports = {
|
|
26
|
+
EmailDesignSetting: ghostBookshelf.model('EmailDesignSetting', EmailDesignSetting)
|
|
27
|
+
};
|
|
@@ -45,26 +45,6 @@ const membersStats = new MembersStats({
|
|
|
45
45
|
let membersApi;
|
|
46
46
|
let verificationTrigger;
|
|
47
47
|
|
|
48
|
-
const sendVerificationEmail = async ({subject, message, amountTriggered}) => {
|
|
49
|
-
const escalationAddress = config.get('hostSettings:emailVerification:escalationAddress');
|
|
50
|
-
const replyTo = config.get('user_email');
|
|
51
|
-
const fromAddress = settingsHelpers.getDefaultEmailAddress();
|
|
52
|
-
|
|
53
|
-
if (escalationAddress) {
|
|
54
|
-
await ghostMailer.send({
|
|
55
|
-
subject,
|
|
56
|
-
html: tpl(message, {
|
|
57
|
-
amountTriggered: amountTriggered,
|
|
58
|
-
siteUrl: urlUtils.getSiteUrl()
|
|
59
|
-
}),
|
|
60
|
-
forceTextContent: true,
|
|
61
|
-
from: fromAddress,
|
|
62
|
-
replyTo,
|
|
63
|
-
to: escalationAddress
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
|
|
68
48
|
const initMembersCSVImporter = ({stripeAPIService}) => {
|
|
69
49
|
return makeMembersCSVImporter({
|
|
70
50
|
storagePath: config.getContentPath('data'),
|
|
@@ -113,8 +93,6 @@ const initVerificationTrigger = () => {
|
|
|
113
93
|
isVerified: () => config.get('hostSettings:emailVerification:verified') === true,
|
|
114
94
|
isVerificationRequired: () => settingsCache.get('email_verification_required') === true,
|
|
115
95
|
setVerificationRequired: value => settingsCache.set('email_verification_required', {value}),
|
|
116
|
-
isVerificationFlowEnabled: () => labsService.isSet('verificationFlow'),
|
|
117
|
-
sendVerificationEmail,
|
|
118
96
|
sendVerificationWebhook: verificationWebhookService.sendVerificationWebhook.bind(verificationWebhookService),
|
|
119
97
|
membersStats,
|
|
120
98
|
Settings: models.Settings,
|
|
@@ -17,8 +17,7 @@ const settingsCache = require('../../../shared/settings-cache');
|
|
|
17
17
|
async function configureApi() {
|
|
18
18
|
const cfg = getConfig({settingsHelpers, config, urlUtils});
|
|
19
19
|
if (cfg) {
|
|
20
|
-
|
|
21
|
-
cfg.testEnv = process.env.NODE_ENV.startsWith('test') && process.env.NODE_ENV !== 'testing-browser';
|
|
20
|
+
cfg.testEnv = process.env.NODE_ENV.startsWith('test');
|
|
22
21
|
await module.exports.configure(cfg);
|
|
23
22
|
return true;
|
|
24
23
|
}
|
|
@@ -47,11 +47,11 @@ class VerificationWebhookService {
|
|
|
47
47
|
async sendVerificationWebhook({ amountTriggered, threshold, method }) {
|
|
48
48
|
const { webhookType, webhookUrl, webhookSecret, siteId } = this.#readWebhookConfig();
|
|
49
49
|
if (typeof webhookUrl !== 'string' || webhookUrl.length === 0) {
|
|
50
|
-
this.#logging.warn('Verification webhook
|
|
50
|
+
this.#logging.warn('Verification webhook is not configured because webhookUrl is missing.');
|
|
51
51
|
return false;
|
|
52
52
|
}
|
|
53
53
|
if (typeof webhookType !== 'string' || webhookType.length === 0) {
|
|
54
|
-
this.#logging.warn('Verification webhook
|
|
54
|
+
this.#logging.warn('Verification webhook is not configured because webhookType is missing.');
|
|
55
55
|
return false;
|
|
56
56
|
}
|
|
57
57
|
const payload = {
|
|
@@ -78,12 +78,12 @@ export class VerificationWebhookService {
|
|
|
78
78
|
const {webhookType, webhookUrl, webhookSecret, siteId} = this.#readWebhookConfig();
|
|
79
79
|
|
|
80
80
|
if (typeof webhookUrl !== 'string' || webhookUrl.length === 0) {
|
|
81
|
-
this.#logging.warn('Verification webhook
|
|
81
|
+
this.#logging.warn('Verification webhook is not configured because webhookUrl is missing.');
|
|
82
82
|
return false;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
if (typeof webhookType !== 'string' || webhookType.length === 0) {
|
|
86
|
-
this.#logging.warn('Verification webhook
|
|
86
|
+
this.#logging.warn('Verification webhook is not configured because webhookType is missing.');
|
|
87
87
|
return false;
|
|
88
88
|
}
|
|
89
89
|
|
|
@@ -3,17 +3,7 @@ const DomainEvents = require('@tryghost/domain-events');
|
|
|
3
3
|
const {MemberCreatedEvent} = require('../../shared/events');
|
|
4
4
|
|
|
5
5
|
const messages = {
|
|
6
|
-
emailVerificationNeeded: `We're hard at work processing your import. To make sure you get great deliverability, we'll need to enable some extra features for your account. A member of our team will be in touch
|
|
7
|
-
emailVerificationEmailSubject: `Email needs verification`,
|
|
8
|
-
emailVerificationEmailMessageImport: `Email verification needed for site: {siteUrl}, has imported: {amountTriggered} members in the last 30 days.`,
|
|
9
|
-
emailVerificationEmailMessageAdmin: `Email verification needed for site: {siteUrl} has added: {amountTriggered} members through the Admin client in the last 30 days.`,
|
|
10
|
-
emailVerificationEmailMessageAPI: `Email verification needed for site: {siteUrl} has added: {amountTriggered} members through the API in the last 30 days.`
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
const verificationMessageBySource = {
|
|
14
|
-
api: messages.emailVerificationEmailMessageAPI,
|
|
15
|
-
admin: messages.emailVerificationEmailMessageAdmin,
|
|
16
|
-
import: messages.emailVerificationEmailMessageImport
|
|
6
|
+
emailVerificationNeeded: `We're hard at work processing your import. To make sure you get great deliverability, we'll need to enable some extra features for your account. A member of our team will be in touch by email to review your account and make sure everything is configured correctly so you're ready to go.`
|
|
17
7
|
};
|
|
18
8
|
|
|
19
9
|
class VerificationTrigger {
|
|
@@ -26,8 +16,6 @@ class VerificationTrigger {
|
|
|
26
16
|
* @param {() => boolean} deps.isVerified Check Ghost config to see if we are already verified
|
|
27
17
|
* @param {() => boolean} deps.isVerificationRequired Check Ghost settings to see whether verification has been requested
|
|
28
18
|
* @param {(value: boolean) => void} deps.setVerificationRequired Directly update the settings cache for email_verification_required
|
|
29
|
-
* @param {() => boolean} deps.isVerificationFlowEnabled Check whether webhook-based verification flow is enabled
|
|
30
|
-
* @param {(content: {subject: string, message: string, amountTriggered: number}) => Promise<void>} deps.sendVerificationEmail Sends an email to the escalation address to confirm that customer needs to be verified
|
|
31
19
|
* @param {(content: {amountTriggered: number, threshold: number, method: string}) => Promise<boolean>} deps.sendVerificationWebhook Sends a webhook to the escalation service to confirm that customer needs to be verified
|
|
32
20
|
* @param {any} deps.Settings Ghost Settings model
|
|
33
21
|
* @param {any} deps.eventRepository For querying events
|
|
@@ -39,8 +27,6 @@ class VerificationTrigger {
|
|
|
39
27
|
isVerified,
|
|
40
28
|
isVerificationRequired,
|
|
41
29
|
setVerificationRequired,
|
|
42
|
-
isVerificationFlowEnabled,
|
|
43
|
-
sendVerificationEmail,
|
|
44
30
|
sendVerificationWebhook,
|
|
45
31
|
Settings,
|
|
46
32
|
eventRepository
|
|
@@ -51,9 +37,7 @@ class VerificationTrigger {
|
|
|
51
37
|
this._isVerified = isVerified;
|
|
52
38
|
this._isVerificationRequired = isVerificationRequired;
|
|
53
39
|
this._setVerificationRequired = setVerificationRequired || (() => {});
|
|
54
|
-
this.
|
|
55
|
-
this._sendVerificationEmail = sendVerificationEmail;
|
|
56
|
-
this._sendVerificationWebhook = sendVerificationWebhook;
|
|
40
|
+
this._sendVerificationWebhook = sendVerificationWebhook || (async () => false);
|
|
57
41
|
this._Settings = Settings;
|
|
58
42
|
this._eventRepository = eventRepository;
|
|
59
43
|
|
|
@@ -76,13 +60,9 @@ class VerificationTrigger {
|
|
|
76
60
|
return this._getImportTriggerThreshold();
|
|
77
61
|
}
|
|
78
62
|
|
|
79
|
-
_shouldUseWebhookFlow() {
|
|
80
|
-
return this._isVerificationFlowEnabled() && typeof this._sendVerificationWebhook === 'function';
|
|
81
|
-
}
|
|
82
|
-
|
|
83
63
|
/**
|
|
84
64
|
*
|
|
85
|
-
* @param {MemberCreatedEvent} event
|
|
65
|
+
* @param {InstanceType<typeof MemberCreatedEvent>} event
|
|
86
66
|
*/
|
|
87
67
|
async _handleMemberCreatedEvent(event) {
|
|
88
68
|
const source = event.data?.source;
|
|
@@ -94,25 +74,29 @@ class VerificationTrigger {
|
|
|
94
74
|
sourceThreshold = this._adminTriggerThreshold;
|
|
95
75
|
}
|
|
96
76
|
|
|
97
|
-
if (['api', 'admin'].includes(source) && isFinite(sourceThreshold)) {
|
|
77
|
+
if (['api', 'admin'].includes(source) && Number.isFinite(sourceThreshold)) {
|
|
98
78
|
const createdAt = new Date();
|
|
99
79
|
createdAt.setDate(createdAt.getDate() - 30);
|
|
100
80
|
const events = await this._eventRepository.getSignupEvents({}, {
|
|
101
|
-
source
|
|
81
|
+
source,
|
|
102
82
|
created_at: {
|
|
103
83
|
$gt: createdAt.toISOString().replace('T', ' ').substring(0, 19)
|
|
104
84
|
}
|
|
105
85
|
});
|
|
106
86
|
|
|
87
|
+
// TODO: Fix off-by-one issue in event dispatch: https://linear.app/ghost/issue/BER-3507/off-by-one-errors-in-event-query-pagination
|
|
88
|
+
const addOneForCurrentEvent = events.meta.pagination.total < events.meta.pagination.limit && events.data.length !== events.meta.pagination.total;
|
|
89
|
+
const currentImport = events.meta.pagination.total + (addOneForCurrentEvent ? 1 : 0);
|
|
90
|
+
|
|
107
91
|
const membersTotal = (await this._eventRepository.getSignupEvents({}, {
|
|
108
92
|
source: 'member'
|
|
109
93
|
})).meta.pagination.total;
|
|
110
94
|
|
|
111
95
|
const effectiveThreshold = Math.max(sourceThreshold, membersTotal);
|
|
112
96
|
|
|
113
|
-
if (
|
|
97
|
+
if (currentImport > effectiveThreshold) {
|
|
114
98
|
await this._startVerificationProcess({
|
|
115
|
-
amount:
|
|
99
|
+
amount: currentImport,
|
|
116
100
|
threshold: effectiveThreshold,
|
|
117
101
|
method: source,
|
|
118
102
|
throwOnTrigger: false,
|
|
@@ -148,24 +132,9 @@ class VerificationTrigger {
|
|
|
148
132
|
};
|
|
149
133
|
}
|
|
150
134
|
|
|
151
|
-
async _startLegacyEmailVerificationProcess({amount, triggerSource, throwOnTrigger}) {
|
|
152
|
-
// GA removal point: delete this method once webhook delivery fully replaces email escalation.
|
|
153
|
-
const verificationMessage = verificationMessageBySource[triggerSource] || messages.emailVerificationEmailMessageImport;
|
|
154
|
-
|
|
155
|
-
await this._markVerificationRequired();
|
|
156
|
-
|
|
157
|
-
await this._sendVerificationEmail({
|
|
158
|
-
message: verificationMessage,
|
|
159
|
-
subject: messages.emailVerificationEmailSubject,
|
|
160
|
-
amountTriggered: amount
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
return this._finishTrigger(throwOnTrigger);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
135
|
async getImportThreshold() {
|
|
167
136
|
const volumeThreshold = this._importTriggerThreshold;
|
|
168
|
-
if (!isFinite(volumeThreshold)) {
|
|
137
|
+
if (!Number.isFinite(volumeThreshold)) {
|
|
169
138
|
return volumeThreshold;
|
|
170
139
|
}
|
|
171
140
|
|
|
@@ -186,7 +155,7 @@ class VerificationTrigger {
|
|
|
186
155
|
}
|
|
187
156
|
|
|
188
157
|
async testImportThreshold() {
|
|
189
|
-
if (!isFinite(this._importTriggerThreshold)) {
|
|
158
|
+
if (!Number.isFinite(this._importTriggerThreshold)) {
|
|
190
159
|
// Infinite threshold, quick path
|
|
191
160
|
return;
|
|
192
161
|
}
|
|
@@ -210,16 +179,18 @@ class VerificationTrigger {
|
|
|
210
179
|
}
|
|
211
180
|
});
|
|
212
181
|
|
|
182
|
+
const currentImport = events.meta.pagination.total;
|
|
183
|
+
|
|
213
184
|
const membersTotal = (await this._eventRepository.getSignupEvents({}, {
|
|
214
185
|
source: 'member'
|
|
215
186
|
})).meta.pagination.total;
|
|
216
187
|
|
|
217
188
|
// Import threshold is either the total number of members (discounting any created by imports in
|
|
218
189
|
// the last 30 days) or the threshold defined in config, whichever is greater.
|
|
219
|
-
const importThreshold = Math.max(membersTotal -
|
|
220
|
-
if (isFinite(importThreshold) &&
|
|
190
|
+
const importThreshold = Math.max(membersTotal - currentImport, this._importTriggerThreshold);
|
|
191
|
+
if (Number.isFinite(importThreshold) && currentImport > importThreshold) {
|
|
221
192
|
await this._startVerificationProcess({
|
|
222
|
-
amount:
|
|
193
|
+
amount: currentImport,
|
|
223
194
|
threshold: importThreshold,
|
|
224
195
|
method: 'import',
|
|
225
196
|
throwOnTrigger: false,
|
|
@@ -254,36 +225,30 @@ class VerificationTrigger {
|
|
|
254
225
|
return {needsVerification: false};
|
|
255
226
|
}
|
|
256
227
|
|
|
257
|
-
// Only trigger
|
|
228
|
+
// Only trigger verification once.
|
|
258
229
|
if (this._isVerificationRequired()) {
|
|
259
230
|
return {needsVerification: false};
|
|
260
231
|
}
|
|
261
232
|
|
|
262
|
-
|
|
233
|
+
let webhookWasSent = false;
|
|
263
234
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
235
|
+
try {
|
|
236
|
+
webhookWasSent = await this._sendVerificationWebhook({
|
|
237
|
+
amountTriggered: amount,
|
|
238
|
+
threshold: threshold ?? amount,
|
|
239
|
+
method: method ?? source ?? 'import'
|
|
240
|
+
});
|
|
241
|
+
} catch (error) {
|
|
242
|
+
// `sendVerificationWebhook` already logs delivery failures.
|
|
243
|
+
return {needsVerification: false};
|
|
244
|
+
}
|
|
271
245
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
return this._finishTrigger(throwOnTrigger);
|
|
275
|
-
}
|
|
276
|
-
} catch (error) {
|
|
277
|
-
// `sendVerificationWebhook` already logs delivery failures.
|
|
278
|
-
}
|
|
246
|
+
if (!webhookWasSent) {
|
|
247
|
+
return {needsVerification: false};
|
|
279
248
|
}
|
|
280
249
|
|
|
281
|
-
|
|
282
|
-
return
|
|
283
|
-
amount,
|
|
284
|
-
triggerSource,
|
|
285
|
-
throwOnTrigger
|
|
286
|
-
});
|
|
250
|
+
await this._markVerificationRequired();
|
|
251
|
+
return this._finishTrigger(throwOnTrigger);
|
|
287
252
|
}
|
|
288
253
|
}
|
|
289
254
|
|
|
@@ -91,7 +91,7 @@ class WebhookTrigger {
|
|
|
91
91
|
error: `Request failed: ${err.code || 'unknown'}`
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
-
logging.
|
|
94
|
+
logging.error(`[WEBHOOK_DELIVERY_FAILURE] url=${webhook.get('target_url') || 'unknown'} status=${err.statusCode || 'none'} error_code=${err.code || 'unknown'} message=${err.message || ''}`, err);
|
|
95
95
|
};
|
|
96
96
|
}
|
|
97
97
|
|
package/core/shared/labs.js
CHANGED
|
@@ -47,11 +47,11 @@ const PRIVATE_FEATURES = [
|
|
|
47
47
|
'emailUniqueid',
|
|
48
48
|
'themeTranslation',
|
|
49
49
|
'indexnow',
|
|
50
|
-
'verificationFlow',
|
|
51
50
|
'membersForward',
|
|
52
51
|
'welcomeEmailsDesignCustomization',
|
|
53
52
|
'pictureImageFormats',
|
|
54
|
-
'smarterCounts'
|
|
53
|
+
'smarterCounts',
|
|
54
|
+
'giftSubscriptions'
|
|
55
55
|
];
|
|
56
56
|
|
|
57
57
|
module.exports.GA_KEYS = [...GA_FEATURES];
|