ghost 6.54.1 → 6.56.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/tryghost-adapter-base-scheduling-0.2.0.tgz +0 -0
- package/components/tryghost-custom-field-types-0.0.0.tgz +0 -0
- package/components/tryghost-kg-default-nodes-2.1.4.tgz +0 -0
- package/components/tryghost-kg-default-transforms-1.3.3.tgz +0 -0
- package/components/tryghost-kg-html-to-lexical-1.3.3.tgz +0 -0
- package/components/tryghost-kg-lexical-html-renderer-1.4.3.tgz +0 -0
- package/components/tryghost-nql-string-0.0.0.tgz +0 -0
- package/core/boot.js +3 -6
- package/core/built/admin/assets/{Combination-CJG7Flp-.js → Combination-BCm9FL4o.js} +1 -1
- package/core/built/admin/assets/{PolarAngleAxis-BGjf7sBu.js → PolarAngleAxis-C4qtHy1B.js} +1 -1
- package/core/built/admin/assets/{RadialBarChart-zeFf9mBz.js → RadialBarChart-Bh09Stsj.js} +1 -1
- package/core/built/admin/assets/{account-migration-BklF2I_I.js → account-migration-D3O_yVck.js} +1 -1
- package/core/built/admin/assets/activitypub/{account-migration-CNovr6W-.js → account-migration-D8FbP-Z-.js} +7 -7
- package/core/built/admin/assets/activitypub/activitypub.js +6 -6
- package/core/built/admin/assets/activitypub/{avatar-B3DEDB1l.js → avatar-11EA1VSs.js} +2 -2
- package/core/built/admin/assets/activitypub/{avatar-flipboard-Cqrg0XMx.js → avatar-flipboard-jfbLG-xe.js} +2 -2
- package/core/built/admin/assets/activitypub/{bluesky-sharing-Ceslb1_M.js → bluesky-sharing-f1jTQe2P.js} +16 -16
- package/core/built/admin/assets/activitypub/{content-formatters-BpQARh28.js → content-formatters-nmTOPY9M.js} +3 -3
- package/core/built/admin/assets/activitypub/{copy-D9UWIAan.js → copy-BCwo9ep3.js} +2 -2
- package/core/built/admin/assets/activitypub/{dist-DHAjBcV3.js → dist-BEfX-0aW.js} +12 -12
- package/core/built/admin/assets/activitypub/{domain-C1qQvJ8C.js → domain-AaDlZlrA.js} +9 -9
- package/core/built/admin/assets/activitypub/{edit-profile-BcH-jEfM.js → edit-profile-DPi8Tl5M.js} +945 -945
- package/core/built/admin/assets/activitypub/{explore-AxwBh9dI.js → explore-CxTzjILQ.js} +8 -8
- package/core/built/admin/assets/activitypub/{feed-BaOlbh7a.js → feed-DPv5x2MX.js} +33 -33
- package/core/built/admin/assets/activitypub/{field-B7mYYQpN.js → field-DbgA8lat.js} +3 -3
- package/core/built/admin/assets/activitypub/{hash-BC9Q0N4c.js → hash-BWuZoYkA.js} +2 -2
- package/core/built/admin/assets/activitypub/{inbox-DQ4ZnC1m.js → inbox-35VDQ2K2.js} +2 -2
- package/core/built/admin/assets/activitypub/{inbox-vXez-5uJ.js → inbox-DSrF_i1O.js} +85 -85
- package/core/built/admin/assets/activitypub/{label-Chv5T8bw.js → label-CPjFBbo7.js} +3 -3
- package/core/built/admin/assets/activitypub/layout-CTgrezN7.js +1 -0
- package/core/built/admin/assets/activitypub/{moderation-DtSM2i1I.js → moderation-BkDC1XVZ.js} +11 -11
- package/core/built/admin/assets/activitypub/{note-Dckp4S-2.js → note-xEyH4xT-.js} +7 -7
- package/core/built/admin/assets/activitypub/{notifications-CG5T22vg.js → notifications-DClaL3ui.js} +9 -9
- package/core/built/admin/assets/activitypub/{preferences-D_799jdl.js → preferences-DpnOa-4Z.js} +20 -20
- package/core/built/admin/assets/activitypub/{profile-D2JfXBQl.js → profile-Dvi9mz-6.js} +237 -237
- package/core/built/admin/assets/activitypub/{reply-CpdMeAO5.js → reply-Cs9ySd6Z.js} +2 -2
- package/core/built/admin/assets/activitypub/{routes-CCHe2dQp.js → routes-DFBeTnfn.js} +1139 -1150
- package/core/built/admin/assets/activitypub/{separator-BI1FLsYM.js → separator-C5u_VjzY.js} +3 -3
- package/core/built/admin/assets/activitypub/{settings-kx-9Enkz.js → settings-CV2fFu6D.js} +13 -13
- package/core/built/admin/assets/activitypub/{site-x4_IjVJ0.js → site-_TgDd_EM.js} +81 -81
- package/core/built/admin/assets/activitypub/{step-1-DIDg4aHb.js → step-1-BfnNL2oF.js} +5 -5
- package/core/built/admin/assets/activitypub/{step-2-BfGe8BYt.js → step-2-DLndZkO-.js} +6 -6
- package/core/built/admin/assets/activitypub/{step-3-mE9spAqw.js → step-3-A-PZTtRE.js} +10 -10
- package/core/built/admin/assets/activitypub/{tabs-CyPRQQ1N.js → tabs-BUbz7tea.js} +321 -319
- package/core/built/admin/assets/activitypub/{topic-filter-DLHBZT93.js → topic-filter-BdX_xYYT.js} +3 -3
- package/core/built/admin/assets/activitypub/{use-activity-pub-queries-VzbN99u_.js → use-activity-pub-queries-EwuRemHh.js} +2 -2
- package/core/built/admin/assets/activitypub/{use-navigate-with-base-path-CAwXTmqH.js → use-navigate-with-base-path-cy4ACBc4.js} +92 -90
- package/core/built/admin/assets/activitypub/{use-reply-chain-data-BAB6fNGt.js → use-reply-chain-data-CyFcGfan.js} +5 -5
- package/core/built/admin/assets/activitypub/{x-C8tAnXly.js → x-7zk8mV2p.js} +2 -2
- package/core/built/admin/assets/audience-CkBb2LlO.js +1 -0
- package/core/built/admin/assets/{automated-emails-of1MvVsv.js → automated-emails-6t2bIRWf.js} +2 -2
- package/core/built/admin/assets/{automations-B7-y3dXt.js → automations-DhKeS_U5.js} +1 -1
- package/core/built/admin/assets/{automations-DRFjW5fw.js → automations-cMllU545.js} +1 -1
- package/core/built/admin/assets/{bluesky-sharing-BbbjWdzs.js → bluesky-sharing-DPGpyD-G.js} +1 -1
- package/core/built/admin/assets/{box-BWbhXh21.js → box-By_EgGtm.js} +1 -1
- package/core/built/admin/assets/{breadcrumb-B-M9Ip_2.js → breadcrumb-DIfcF5ow.js} +1 -1
- package/core/built/admin/assets/build-CsiH26uS.js +1 -0
- package/core/built/admin/assets/{card-DGoDJzrn.js → card-aUHX0hyR.js} +1 -1
- package/core/built/admin/assets/{chart-BuRT2Y6p.js → chart-BnXyfH11.js} +1 -1
- package/core/built/admin/assets/{chart-helpers-xiSsW8r0.js → chart-helpers-DV0FkdcD.js} +1 -1
- package/core/built/admin/assets/{checkbox-EhJKHFdZ.js → checkbox-BIl077a0.js} +1 -1
- package/core/built/admin/assets/{chunk.361.3cdb584b973959e52ccc.js → chunk.332.05991a101d67f732dec3.js} +16 -12
- package/core/built/admin/assets/chunk.524.87b52f4fada529df2c4c.js +25 -0
- package/core/built/admin/assets/chunk.582.340bcca873e806cbd4bb.js +27 -0
- package/core/built/admin/assets/{code-editor-view-CZtJFGhR.js → code-editor-view-C0aWNwdl.js} +1 -1
- package/core/built/admin/assets/comments-qwhIjH3c.js +1 -0
- package/core/built/admin/assets/{container-BYz2Ed39.js → container-LiZf3XdY.js} +1 -1
- package/core/built/admin/assets/{data-list-16rkJYYi.js → data-list-CupF7bjM.js} +1 -1
- package/core/built/admin/assets/{date-range-select-CTlyo8zW.js → date-range-select-dXc-GsVe.js} +1 -1
- package/core/built/admin/assets/{disabled-sources-indicator-qs2GfLvl.js → disabled-sources-indicator-DYAXW-6V.js} +1 -1
- package/core/built/admin/assets/dist-BUKmD51u.js +1 -0
- package/core/built/admin/assets/{dist-2Tnz2Yfq.js → dist-CNlMj7ML.js} +1 -1
- package/core/built/admin/assets/{dist-DGzPJ_bO.js → dist-Cb5RSHSg.js} +1 -1
- package/core/built/admin/assets/{dist-P_ohUlTi.js → dist-CqO2lWS0.js} +1 -1
- package/core/built/admin/assets/{domain-CPNXKVOV.js → domain-B5ogXUBa.js} +1 -1
- package/core/built/admin/assets/{dropzone-BmCXnw6J.js → dropzone-BnFG7dDP.js} +1 -1
- package/core/built/admin/assets/{edit-profile-SGrJve5M.js → edit-profile-B07jCZy9.js} +1 -1
- package/core/built/admin/assets/editor-DDYSOXGN.js +8 -0
- package/core/built/admin/assets/{empty-indicator-oX1u1Pkn.js → empty-indicator-WB1WgT9h.js} +1 -1
- package/core/built/admin/assets/{explore-CUYLv9G9.js → explore-Ca1y8HW_.js} +1 -1
- package/core/built/admin/assets/{feed-CPdJMttL.js → feed-x4iFMBP4.js} +1 -1
- package/core/built/admin/assets/field-ChCAabZg.js +1 -0
- package/core/built/admin/assets/filter-sources-LdtH1e-T.js +1 -0
- package/core/built/admin/assets/{filters-bXkkWro8.js → filters-CLehPo_D.js} +1 -1
- package/core/built/admin/assets/filters-CjVKRP8U.js +1 -0
- package/core/built/admin/assets/{ghost-05f9a7182ed56903dba05878a5b8422b.js → ghost-7f01131115186d1af84ead76e79646dd.js} +275 -255
- package/core/built/admin/assets/{ghost-3063eae29e6c9cc6d6e7131928a27da0.css → ghost-d38c30b866e59f2e06a357b5eb844fe5.css} +1 -1
- package/core/built/admin/assets/{ghost-dark-c8d0ab7f515b2010c173e23e9b32ea8a.css → ghost-dark-1bffd5099ba7a749303133efacf8480b.css} +1 -1
- package/core/built/admin/assets/gift-link-modal-Buqh2vXh.js +1 -0
- package/core/built/admin/assets/{gift-links-nBxJjZiQ.js → gift-links-CsUhefWG.js} +1 -1
- package/core/built/admin/assets/growth-DajOBdN0.js +1 -0
- package/core/built/admin/assets/growth-Dy2UOSay.js +3 -0
- package/core/built/admin/assets/{hooks-BApwwBE0.js → hooks-CixO01lL.js} +1 -1
- package/core/built/admin/assets/{inbox-Xx1CdnfO.js → inbox-BUOLDoji.js} +13 -13
- package/core/built/admin/assets/index-08n05-rc.js +67 -0
- package/core/built/admin/assets/index-DgUzgFvS.css +1 -0
- package/core/built/admin/assets/{inline-EMAm3okk.js → inline-Cw-gigDW.js} +1 -1
- package/core/built/admin/assets/input-group-uSK3XT7b.js +1 -0
- package/core/built/admin/assets/{input-surface-BpAjZXY5.js → input-surface-BdOe4KzO.js} +1 -1
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +9132 -9087
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +121 -121
- package/core/built/admin/assets/koenig-lexical/style.css +1 -1
- package/core/built/admin/assets/{koenig-lexical-B61IALf5.js → koenig-lexical-BFc4yFed.js} +82 -82
- package/core/built/admin/assets/{kpi-card-DCdlyaSS.js → kpi-card-BN-E6WYX.js} +1 -1
- package/core/built/admin/assets/{kpi-card-BlZaG1Lo.js → kpi-card-CCt-qeWD.js} +1 -1
- package/core/built/admin/assets/{kpi-helpers-DpAuS1Nb.js → kpi-helpers-DlxTbTs4.js} +1 -1
- package/core/built/admin/assets/{kpi-tabs-Dme44bQS.js → kpi-tabs-BlcUttpn.js} +1 -1
- package/core/built/admin/assets/{label-dSs5aepE.js → label-AVrxwmsz.js} +1 -1
- package/core/built/admin/assets/label-picker-DVnpu_IF.js +1 -0
- package/core/built/admin/assets/labels-DAIqKed7.js +1 -0
- package/core/built/admin/assets/layout-B0YLh7R9.js +1 -0
- package/core/built/admin/assets/link-helpers-D9LsDOkG.js +1 -0
- package/core/built/admin/assets/{list-page-B6ANtzkd.js → list-page-CdsnQR9N.js} +1 -1
- package/core/built/admin/assets/member-custom-fields-C8NpZYza.js +1 -0
- package/core/built/admin/assets/member-detail-CIVAdKXf.js +1 -0
- package/core/built/admin/assets/members-oLPiSkXD.js +10 -0
- package/core/built/admin/assets/modals-CB7v-k3T.js +31 -0
- package/core/built/admin/assets/moderation-CQ3urTFE.js +1 -0
- package/core/built/admin/assets/newsletter-rbeDX9Wk.js +1 -0
- package/core/built/admin/assets/{newsletters-D1jbtV-h.js → newsletters-BXJbJgA-.js} +1 -1
- package/core/built/admin/assets/newsletters-vt3Ni1Ov.js +1 -0
- package/core/built/admin/assets/note-Dk877BTv.js +1 -0
- package/core/built/admin/assets/notifications-CPlLjPxO.js +1 -0
- package/core/built/admin/assets/{offers-CrY6nSRR.js → offers-DEniVRD5.js} +1 -1
- package/core/built/admin/assets/onboarding-route-DdStOtjy.js +1 -0
- package/core/built/admin/assets/overview--4V-kx9c.js +1 -0
- package/core/built/admin/assets/overview-DSLv30oz.js +1 -0
- package/core/built/admin/assets/{page-header-op0eN36I.js → page-header-pp4dWmOS.js} +1 -1
- package/core/built/admin/assets/{pages-BbNWFcGj.js → pages-B4goRPUn.js} +1 -1
- package/core/built/admin/assets/{post-analytics-WwRDEnrV.js → post-analytics-BMJrI81R.js} +1 -1
- package/core/built/admin/assets/post-analytics-header-pU2m9ZuV.js +1 -0
- package/core/built/admin/assets/post-analytics-provider-Btml5vJI.js +1 -0
- package/core/built/admin/assets/{post-share-modal-Dx-MEjhI.js → post-share-modal-AcUZkbfL.js} +1 -1
- package/core/built/admin/assets/{posts-EWpZIjkr.js → posts-DhXE7b6r.js} +1 -1
- package/core/built/admin/assets/preferences-Ru4CRPHC.js +5 -0
- package/core/built/admin/assets/profile-BNb1oj7D.js +1 -0
- package/core/built/admin/assets/{referrers-BCfFYSHc.js → referrers-pmJT4k3-.js} +1 -1
- package/core/built/admin/assets/{router-provider-C1R4AZ2o.js → router-provider-BmEZf6c6.js} +1 -1
- package/core/built/admin/assets/select-B0HteHrp.js +1 -0
- package/core/built/admin/assets/settings-B1C9Q3wo.js +4 -0
- package/core/built/admin/assets/{settings-H6HxFgfI.js → settings-BGgk7oCI.js} +1 -1
- package/core/built/admin/assets/{shade-app-B3heOAfz-CGXQwWeO.js → shade-app-B3heOAfz-BhQXxNkT.js} +1 -1
- package/core/built/admin/assets/{share-modal-BJCadJyp.js → share-modal-BeuydaBf.js} +1 -1
- package/core/built/admin/assets/social-urls-DA2lXrhW.js +8 -0
- package/core/built/admin/assets/{sort-button-DyYlfYp7.js → sort-button-Df4t2AL8.js} +1 -1
- package/core/built/admin/assets/{source-icon-_ubRDbg_.js → source-icon-ajoaKb41.js} +1 -1
- package/core/built/admin/assets/{stack-xiN0eQ6Z.js → stack-CIfOAAVf.js} +1 -1
- package/core/built/admin/assets/{stats-BQZMGMvn.js → stats-C_lfxZAw.js} +1 -1
- package/core/built/admin/assets/{stats-view-Doei-qCX.js → stats-view-Cxot1iD8.js} +1 -1
- package/core/built/admin/assets/{step-1-IKiLBPoC.js → step-1-ogAA4lDr.js} +1 -1
- package/core/built/admin/assets/{step-2-jJ41PsDN.js → step-2-C5Xini-7.js} +1 -1
- package/core/built/admin/assets/{step-3-CaqFAEqf.js → step-3-CZv1FV81.js} +2 -2
- package/core/built/admin/assets/{switch-t-WyENU2.js → switch-CyQcwMAv.js} +1 -1
- package/core/built/admin/assets/{table-DxUL78jU.js → table-DFj1wiYv.js} +1 -1
- package/core/built/admin/assets/tabs-D4y1gqUd.js +1 -0
- package/core/built/admin/assets/{tags-CNujGwkT.js → tags-Dmft-Y4O.js} +1 -1
- package/core/built/admin/assets/{text-DwtjwGkh.js → text-DU9Hv33_.js} +1 -1
- package/core/built/admin/assets/textarea-d9qEkMOP.js +1 -0
- package/core/built/admin/assets/{tiers-BgYLWRJv.js → tiers-DYsa4DCo.js} +1 -1
- package/core/built/admin/assets/{toggle-group-BeEpQ69p.js → toggle-group-R2QMFvXW.js} +1 -1
- package/core/built/admin/assets/{topic-filter-Bb5MOD-1.js → topic-filter-DGDxDalo.js} +1 -1
- package/core/built/admin/assets/{use-analytics-data-Cds6XDjg.js → use-analytics-data-C76v4LIp.js} +1 -1
- package/core/built/admin/assets/use-filter-params-WrdhAISv.js +1 -0
- package/core/built/admin/assets/{use-growth-stats-D8uf98-G.js → use-growth-stats-aNXJOmQr.js} +1 -1
- package/core/built/admin/assets/{use-post-referrers-CRtgyuXB.js → use-post-referrers-CG0SZioj.js} +1 -1
- package/core/built/admin/assets/{use-reply-chain-data-DbKYX50v.js → use-reply-chain-data-RLdPKRmS.js} +1 -1
- package/core/built/admin/assets/{use-simple-pagination-DC01kpo0.js → use-simple-pagination-DgcF69KI.js} +1 -1
- package/core/built/admin/assets/{use-tinybird-query-C7bQRJWw.js → use-tinybird-query-C1R_Kbmy.js} +1 -1
- package/core/built/admin/assets/{use-tinybird-token-Az4tam7_.js → use-tinybird-token-C9fPjVM_.js} +1 -1
- package/core/built/admin/assets/{users-CcSCQFcQ.js → users-Dkv8NQZe.js} +1 -1
- package/core/built/admin/assets/{virtual-list-Dhuh9fGx.js → virtual-list-BmTUzeoF.js} +1 -1
- package/core/built/admin/assets/web-BqW0b0_C.js +1 -0
- package/core/built/admin/assets/web-Cy5GKMWM.js +2 -0
- package/core/built/admin/index.html +18 -18
- package/core/frontend/services/data/fetch-data.js +10 -5
- package/core/frontend/services/proxy.js +7 -8
- package/core/frontend/services/routing/api-adapter.js +115 -0
- package/core/frontend/services/routing/api-adapter.ts +157 -0
- package/core/frontend/services/routing/collection-router.js +5 -3
- package/core/frontend/services/routing/controllers/index.js +1 -1
- package/core/frontend/services/routing/controllers/rss.js +26 -31
- package/core/frontend/services/routing/controllers/rss.ts +66 -0
- package/core/frontend/services/routing/controllers/static.js +8 -7
- package/core/frontend/services/routing/parent-router.js +17 -4
- package/core/frontend/services/routing/permalink-adapter.js +26 -0
- package/core/frontend/services/routing/permalink-adapter.ts +23 -0
- package/core/frontend/services/routing/registry.js +59 -33
- package/core/frontend/services/routing/router-manager.js +14 -18
- package/core/frontend/services/routing/static-routes-router.js +11 -13
- package/core/frontend/services/routing/taxonomy-router.js +6 -2
- package/core/server/api/endpoints/automation-action-links.js +68 -0
- package/core/server/api/endpoints/automation-action-links.ts +46 -0
- package/core/server/api/endpoints/index.js +4 -0
- package/core/server/api/endpoints/settings.js +0 -2
- package/core/server/api/endpoints/utils/serializers/output/utils/url.js +11 -1
- package/core/server/data/migrations/versions/6.55/2026-07-27-11-25-44-add-to-hash-to-redirects.js +51 -0
- package/core/server/data/migrations/versions/6.55/2026-07-28-12-08-00-remove-routes-hash-from-settings.js +5 -0
- package/core/server/data/migrations/versions/6.56/2026-08-03-19-27-51-add-automation-run-step-id-to-automated-email-recipients.js +8 -0
- package/core/server/data/migrations/versions/6.56/2026-08-04-14-42-39-restrict-notification-add-permission-to-administrators.js +12 -0
- package/core/server/data/schema/commands.js +5 -0
- package/core/server/data/schema/default-settings/default-settings.json +0 -4
- package/core/server/data/schema/fixtures/fixtures.json +2 -2
- package/core/server/data/schema/schema.js +6 -1
- package/core/server/models/post.js +10 -1
- package/core/server/services/automations/automations-api.js +15 -0
- package/core/server/services/automations/automations-api.ts +19 -0
- package/core/server/services/automations/automations-repository.ts +20 -0
- package/core/server/services/automations/database-automations-repository.js +132 -30
- package/core/server/services/automations/database-automations-repository.ts +141 -12
- package/core/server/services/automations/database-date.js +10 -0
- package/core/server/services/automations/database-date.ts +5 -0
- package/core/server/services/automations/get-scheduler-idempotency-key.js +13 -0
- package/core/server/services/automations/get-scheduler-idempotency-key.ts +8 -0
- package/core/server/services/automations/poll.js +7 -2
- package/core/server/services/automations/poll.ts +10 -2
- package/core/server/services/automations/scheduler-poll-time.js +40 -0
- package/core/server/services/automations/scheduler-poll-time.ts +38 -0
- package/core/server/services/automations/service.js +19 -3
- package/core/server/services/automations/service.ts +20 -3
- package/core/server/services/comments/comments-service-emails.js +10 -7
- package/core/server/services/comments/comments-service.js +9 -4
- package/core/server/services/email-rendering/partials/email-wrapper.hbs +3 -1
- package/core/server/services/email-service/email-service-wrapper.js +6 -1
- package/core/server/services/email-service/email-templates/partials/styles.hbs +8 -0
- package/core/server/services/email-service/newsletter-email-event-storage.js +41 -10
- package/core/server/services/gift-links/CONTEXT.md +9 -0
- package/core/server/services/gifts/CONTEXT.md +55 -0
- package/core/server/services/gifts/README.md +28 -0
- package/core/server/services/gifts/gift-bookshelf-repository.js +65 -45
- package/core/server/services/gifts/gift-bookshelf-repository.ts +203 -74
- package/core/server/services/gifts/gift-checkout-adapter.js +59 -0
- package/core/server/services/gifts/gift-checkout-offer.js +74 -0
- package/core/server/services/gifts/gift-checkout-offer.ts +127 -0
- package/core/server/services/gifts/gift-codec.js +63 -0
- package/core/server/services/gifts/gift-codec.ts +62 -0
- package/core/server/services/gifts/gift-controller.js +6 -31
- package/core/server/services/gifts/gift-controller.ts +6 -69
- package/core/server/services/gifts/gift-email-service.ts +3 -2
- package/core/server/services/gifts/gift-schema.js +34 -0
- package/core/server/services/gifts/gift-schema.ts +39 -0
- package/core/server/services/gifts/gift-service-wrapper.js +13 -11
- package/core/server/services/gifts/gift-service.js +265 -52
- package/core/server/services/gifts/gift-service.ts +431 -97
- package/core/server/services/gifts/gift.ts +16 -39
- package/core/server/services/lib/dynamic-redirect-manager.js +12 -3
- package/core/server/services/link-redirection/link-redirect-repository.js +41 -4
- package/core/server/services/link-redirection/link-redirect.js +2 -0
- package/core/server/services/link-redirection/link-redirect.ts +3 -1
- package/core/server/services/link-redirection/link-redirects-service.js +38 -2
- package/core/server/services/link-tracking/click-event.js +3 -0
- package/core/server/services/link-tracking/index.js +38 -6
- package/core/server/services/link-tracking/link-click-repository.js +35 -9
- package/core/server/services/link-tracking/link-click-tracking-service.js +54 -4
- package/core/server/services/member-attribution/outbound-link-tagger.js +3 -1
- package/core/server/services/member-welcome-emails/member-welcome-email-renderer.js +20 -4
- package/core/server/services/member-welcome-emails/service.js +15 -3
- package/core/server/services/members/api.js +1 -2
- package/core/server/services/members/import-export/csv/formula.js +16 -0
- package/core/server/services/members/import-export/csv/formula.ts +14 -0
- package/core/server/services/members/import-export/csv/index.js +3 -1
- package/core/server/services/members/import-export/csv/index.ts +1 -0
- package/core/server/services/members/import-export/import/completion-email.js +22 -3
- package/core/server/services/members/import-export/import/completion-email.ts +29 -7
- package/core/server/services/members/import-export/import/importer.js +25 -4
- package/core/server/services/members/import-export/import/importer.ts +44 -5
- package/core/server/services/members/import-export/index.js +18 -1
- package/core/server/services/members/import-export/index.ts +32 -3
- package/core/server/services/members/members-api/controllers/router-controller.js +38 -36
- package/core/server/services/members/members-api/members-api.js +13 -7
- package/core/server/services/members/members-api/repositories/event-repository.js +4 -94
- package/core/server/services/members/members-api/services/member-bread-service.js +3 -3
- package/core/server/services/members/members-api/services/payments-service.js +6 -68
- package/core/server/services/members/service.js +8 -1
- package/core/server/services/members-custom-fields/schema.js +8 -1
- package/core/server/services/members-custom-fields/schema.ts +8 -0
- package/core/server/services/members-custom-fields/values-service.js +136 -8
- package/core/server/services/members-custom-fields/values-service.ts +152 -10
- package/core/server/services/mentions/service.js +5 -1
- package/core/server/services/oembed/oembed-service.js +44 -3
- package/core/server/services/posts/posts-service.js +11 -0
- package/core/server/services/route-settings/dynamic-routing-service.js +21 -52
- package/core/server/services/route-settings/index.js +1 -16
- package/core/server/services/route-settings/route-settings-parser.js +25 -25
- package/core/server/services/route-settings/route-settings-parser.ts +27 -27
- package/core/server/services/route-settings/validation-errors.js +68 -4
- package/core/server/services/route-settings/validation-errors.ts +79 -4
- package/core/server/services/settings/settings-service.js +0 -18
- package/core/server/services/stats/posts-stats-service.js +27 -23
- package/core/server/services/stripe/services/webhook/charge-refunded-event-service.js +1 -1
- package/core/server/services/stripe/services/webhook/checkout-session-event-service.js +16 -4
- package/core/server/services/stripe/stripe-api.js +10 -0
- package/core/server/services/url/url-service-facade.js +1 -1
- package/core/server/services/url/url-service-facade.ts +8 -1
- package/core/server/web/api/endpoints/admin/routes.js +1 -0
- package/core/server/web/gift-preview/controller.js +10 -25
- package/core/shared/labs.js +2 -2
- package/package.json +8 -7
- package/pnpm-lock.yaml +28 -18
- package/pnpm-workspace.yaml +6 -4
- package/components/tryghost-kg-default-nodes-2.1.3.tgz +0 -0
- package/core/built/admin/assets/Growth-BTP1LthE.js +0 -3
- package/core/built/admin/assets/Newsletters-CSEX0myc.js +0 -1
- package/core/built/admin/assets/Overview-BjACEsJF.js +0 -1
- package/core/built/admin/assets/Web-CxxxjCcv.js +0 -2
- package/core/built/admin/assets/activitypub/layout-CziktJXo.js +0 -1
- package/core/built/admin/assets/audience-X_XsZivI.js +0 -1
- package/core/built/admin/assets/chunk.524.8365cd9280405c0f6216.js +0 -25
- package/core/built/admin/assets/chunk.582.61d078937e6102769f2c.js +0 -27
- package/core/built/admin/assets/comments-CvyXxOyN.js +0 -1
- package/core/built/admin/assets/constants-c-JNF9c2.js +0 -1
- package/core/built/admin/assets/dist-CjY-f5Z1.js +0 -1
- package/core/built/admin/assets/editor-BOG2YPUm.js +0 -8
- package/core/built/admin/assets/field-D0XISCK2.js +0 -1
- package/core/built/admin/assets/filter-sources-Bieps75g.js +0 -1
- package/core/built/admin/assets/filters-CuB-A7ma.js +0 -1
- package/core/built/admin/assets/gift-link-modal-su8MsRdg.js +0 -1
- package/core/built/admin/assets/growth-Ba6S5FQX.js +0 -1
- package/core/built/admin/assets/index-D8PbzuT9.css +0 -1
- package/core/built/admin/assets/index-ukW_0Uie.js +0 -67
- package/core/built/admin/assets/input-group-CGOXcy4l.js +0 -1
- package/core/built/admin/assets/label-picker-Uq91atex.js +0 -1
- package/core/built/admin/assets/labels-Cyayfd-e.js +0 -1
- package/core/built/admin/assets/layout-WUcz6tu1.js +0 -1
- package/core/built/admin/assets/link-helpers-tOlxmuH5.js +0 -1
- package/core/built/admin/assets/member-custom-fields-NCzrMuba.js +0 -1
- package/core/built/admin/assets/member-detail-DPWDtYnN.js +0 -1
- package/core/built/admin/assets/members-B9rO7qr4.js +0 -10
- package/core/built/admin/assets/modals-CxRaan1J.js +0 -31
- package/core/built/admin/assets/moderation-D8zon60E.js +0 -1
- package/core/built/admin/assets/multi-select-combobox-1wYm3nle.js +0 -1
- package/core/built/admin/assets/newsletter-DbYMRxRS.js +0 -1
- package/core/built/admin/assets/note-PiWPnnrh.js +0 -1
- package/core/built/admin/assets/notifications-DEgqqB9G.js +0 -1
- package/core/built/admin/assets/nql-BsgFMd93.js +0 -1
- package/core/built/admin/assets/onboarding-route-BzF6HeVn.js +0 -1
- package/core/built/admin/assets/overview-DixLeIqY.js +0 -1
- package/core/built/admin/assets/post-analytics-header-DsMoLx0R.js +0 -1
- package/core/built/admin/assets/post-analytics-provider-BJCPLMIb.js +0 -1
- package/core/built/admin/assets/preferences-D7-j6A4C.js +0 -5
- package/core/built/admin/assets/profile-Dl69QPp0.js +0 -1
- package/core/built/admin/assets/select-C1qDZnW4.js +0 -1
- package/core/built/admin/assets/settings-BbmC_Iqq.js +0 -4
- package/core/built/admin/assets/social-urls-BPL1nADp.js +0 -8
- package/core/built/admin/assets/tabs-BQcJQ1ke.js +0 -1
- package/core/built/admin/assets/textarea-Bjccvh_E.js +0 -1
- package/core/built/admin/assets/use-filter-params-DjXkhO54.js +0 -1
- package/core/built/admin/assets/web-LtHNiNBV.js +0 -1
- package/core/server/services/gifts/gift-repository.js +0 -2
- package/core/server/services/gifts/gift-repository.ts +0 -29
- package/core/server/services/route-settings/activation-bridge.js +0 -164
- package/core/server/services/route-settings/activation-bridge.ts +0 -210
- package/core/server/services/route-settings/default-settings-manager.js +0 -63
- package/core/server/services/route-settings/route-settings.js +0 -170
- package/core/server/services/route-settings/settings-loader.js +0 -60
- package/core/server/services/route-settings/settings-path-manager.js +0 -47
- package/core/server/services/route-settings/validate.js +0 -440
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {GIFT_EXPIRY_DAYS} from './constants';
|
|
2
|
+
import type {GiftCadence, GiftData, GiftStatus} from './gift-schema';
|
|
2
3
|
|
|
3
|
-
export type GiftStatus
|
|
4
|
-
export type GiftCadence = 'month' | 'year';
|
|
4
|
+
export type {GiftCadence, GiftStatus} from './gift-schema';
|
|
5
5
|
|
|
6
6
|
export type RedeemableCheckFailureReason = 'redeemed' | 'consumed' | 'expired' | 'refunded' | 'paid-member';
|
|
7
7
|
export type RedeemableCheckResult =
|
|
@@ -13,43 +13,20 @@ export type ReassignableCheckResult =
|
|
|
13
13
|
| {reassignable: true}
|
|
14
14
|
| {reassignable: false; reason: ReassignableCheckFailureReason};
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
token
|
|
18
|
-
buyerEmail
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
status: GiftStatus;
|
|
31
|
-
purchasedAt: Date;
|
|
32
|
-
redeemedAt: Date | null;
|
|
33
|
-
consumedAt: Date | null;
|
|
34
|
-
expiredAt: Date | null;
|
|
35
|
-
refundedAt: Date | null;
|
|
36
|
-
consumesSoonReminderSentAt: Date | null;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface GiftFromPurchaseData {
|
|
40
|
-
token: string;
|
|
41
|
-
buyerEmail: string;
|
|
42
|
-
buyerMemberId: string | null;
|
|
43
|
-
tierId: string;
|
|
44
|
-
cadence: GiftCadence;
|
|
45
|
-
duration: number;
|
|
46
|
-
currency: string;
|
|
47
|
-
amount: number;
|
|
48
|
-
stripeCheckoutSessionId: string;
|
|
49
|
-
stripePaymentIntentId: string;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export class Gift {
|
|
16
|
+
export type GiftFromPurchaseData = Pick<GiftData,
|
|
17
|
+
| 'token'
|
|
18
|
+
| 'buyerEmail'
|
|
19
|
+
| 'buyerMemberId'
|
|
20
|
+
| 'tierId'
|
|
21
|
+
| 'cadence'
|
|
22
|
+
| 'duration'
|
|
23
|
+
| 'currency'
|
|
24
|
+
| 'amount'
|
|
25
|
+
| 'stripeCheckoutSessionId'
|
|
26
|
+
| 'stripePaymentIntentId'
|
|
27
|
+
>;
|
|
28
|
+
|
|
29
|
+
export class Gift implements GiftData {
|
|
53
30
|
token: string;
|
|
54
31
|
buyerEmail: string;
|
|
55
32
|
buyerMemberId: string | null;
|
|
@@ -56,14 +56,23 @@ class DynamicRedirectManager {
|
|
|
56
56
|
|
|
57
57
|
this.router.get(fromRegex, (req, res) => {
|
|
58
58
|
const maxAge = permanent ? this.permanentMaxAge : 0;
|
|
59
|
-
const toURL = parseURL(to);
|
|
60
|
-
const toURLParams = parseQuerystring(toURL.query);
|
|
61
59
|
const currentURL = parseURL(req.url);
|
|
62
60
|
const currentURLParams = parseQuerystring(currentURL.query);
|
|
61
|
+
|
|
62
|
+
const toURL = parseURL(to);
|
|
63
|
+
toURL.pathname = currentURL.pathname.replace(fromRegex, toURL.pathname);
|
|
64
|
+
|
|
65
|
+
// Captures are percent-encoded path segments (Express rejects any it can't decode),
|
|
66
|
+
// so decode then re-encode for the query — reserved chars stay literal, no double-encoding
|
|
67
|
+
const captures = currentURL.pathname.match(fromRegex) || [];
|
|
68
|
+
const substitutedQuery = (toURL.query || '').replace(/\$(\d+)/g, (match, n) => (
|
|
69
|
+
captures[n] !== undefined ? encodeURIComponent(decodeURIComponent(captures[n])) : match
|
|
70
|
+
));
|
|
71
|
+
const toURLParams = parseQuerystring(substitutedQuery);
|
|
72
|
+
|
|
63
73
|
const params = Object.assign({}, currentURLParams, toURLParams);
|
|
64
74
|
const search = formatQuerystring(params);
|
|
65
75
|
|
|
66
|
-
toURL.pathname = currentURL.pathname.replace(fromRegex, toURL.pathname);
|
|
67
76
|
toURL.search = search !== '' ? `?${search}` : null;
|
|
68
77
|
|
|
69
78
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
1
2
|
const {LinkRedirect} = require('./link-redirect');
|
|
2
3
|
const ObjectID = require('bson-objectid').default;
|
|
3
4
|
const debug = require('@tryghost/debug')('LinkRedirectRepository');
|
|
@@ -49,7 +50,11 @@ module.exports = class LinkRedirectRepository {
|
|
|
49
50
|
const model = await this.#LinkRedirect.add({
|
|
50
51
|
// Only store the pathname (no support for variable query strings)
|
|
51
52
|
from: this.stripSubdirectoryFromPath(linkRedirect.from.pathname),
|
|
52
|
-
to: linkRedirect.to.href
|
|
53
|
+
to: linkRedirect.to.href,
|
|
54
|
+
...(linkRedirect.automationActionRevisionId ? {
|
|
55
|
+
automation_action_revision_id: linkRedirect.automationActionRevisionId,
|
|
56
|
+
to_hash: this.#getToHash(linkRedirect.to)
|
|
57
|
+
} : {})
|
|
53
58
|
}, {});
|
|
54
59
|
|
|
55
60
|
linkRedirect.link_id = ObjectID.createFromHexString(model.id);
|
|
@@ -82,7 +87,8 @@ module.exports = class LinkRedirectRepository {
|
|
|
82
87
|
id: model.id,
|
|
83
88
|
from: new URL(this.#trimLeadingSlash(model.get('from')), this.#urlUtils.urlFor('home', true)),
|
|
84
89
|
to: new URL(model.get('to')),
|
|
85
|
-
edited
|
|
90
|
+
edited,
|
|
91
|
+
automationActionRevisionId: model.get('automation_action_revision_id')
|
|
86
92
|
});
|
|
87
93
|
}
|
|
88
94
|
|
|
@@ -93,6 +99,7 @@ module.exports = class LinkRedirectRepository {
|
|
|
93
99
|
* @param {string} serialized.from - path of the URL
|
|
94
100
|
* @param {string} serialized.to - URL to redirect to
|
|
95
101
|
* @param {boolean} serialized.edited - whether the link has been edited
|
|
102
|
+
* @param {string} [serialized.automationActionRevisionId] - owning automation action revision
|
|
96
103
|
* @returns {InstanceType<LinkRedirect>} LinkRedirect
|
|
97
104
|
*/
|
|
98
105
|
#fromSerialized(serialized) {
|
|
@@ -100,7 +107,8 @@ module.exports = class LinkRedirectRepository {
|
|
|
100
107
|
id: serialized.link_id,
|
|
101
108
|
from: new URL(this.#trimLeadingSlash(serialized.from), this.#urlUtils.urlFor('home', true)),
|
|
102
109
|
to: new URL(serialized.to),
|
|
103
|
-
edited: serialized.edited
|
|
110
|
+
edited: serialized.edited,
|
|
111
|
+
automationActionRevisionId: serialized.automationActionRevisionId
|
|
104
112
|
});
|
|
105
113
|
}
|
|
106
114
|
|
|
@@ -114,7 +122,10 @@ module.exports = class LinkRedirectRepository {
|
|
|
114
122
|
link_id: linkRedirect.link_id.toHexString(),
|
|
115
123
|
from: linkRedirect.from.pathname,
|
|
116
124
|
to: linkRedirect.to.href,
|
|
117
|
-
edited: linkRedirect.edited
|
|
125
|
+
edited: linkRedirect.edited,
|
|
126
|
+
...(linkRedirect.automationActionRevisionId ? {
|
|
127
|
+
automationActionRevisionId: linkRedirect.automationActionRevisionId
|
|
128
|
+
} : {})
|
|
118
129
|
};
|
|
119
130
|
}
|
|
120
131
|
|
|
@@ -180,6 +191,32 @@ module.exports = class LinkRedirectRepository {
|
|
|
180
191
|
}
|
|
181
192
|
}
|
|
182
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Get an automation LinkRedirect by action revision and destination URL
|
|
196
|
+
* @param {string} automationActionRevisionId
|
|
197
|
+
* @param {Readonly<URL>} url
|
|
198
|
+
* @returns {Promise<InstanceType<LinkRedirect>|undefined>} LinkRedirect
|
|
199
|
+
*/
|
|
200
|
+
async getByAutomationActionRevisionAndURL(automationActionRevisionId, url) {
|
|
201
|
+
const linkRedirectModel = await this.#LinkRedirect.findOne({
|
|
202
|
+
automation_action_revision_id: automationActionRevisionId,
|
|
203
|
+
to_hash: this.#getToHash(url)
|
|
204
|
+
}, {});
|
|
205
|
+
|
|
206
|
+
if (linkRedirectModel) {
|
|
207
|
+
return this.fromModel(linkRedirectModel);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The destination column is too long to index, so unique lookups use its SHA-256 digest
|
|
213
|
+
* @param {Readonly<URL>} url
|
|
214
|
+
* @returns {Buffer}
|
|
215
|
+
*/
|
|
216
|
+
#getToHash(url) {
|
|
217
|
+
return crypto.createHash('sha256').update(url.href).digest();
|
|
218
|
+
}
|
|
219
|
+
|
|
183
220
|
/**
|
|
184
221
|
* Convert root relative URLs to subdirectory relative URLs
|
|
185
222
|
* @param {string} path
|
|
@@ -10,6 +10,7 @@ class LinkRedirect {
|
|
|
10
10
|
from;
|
|
11
11
|
to;
|
|
12
12
|
edited;
|
|
13
|
+
automationActionRevisionId;
|
|
13
14
|
constructor(data) {
|
|
14
15
|
if (!data.id) {
|
|
15
16
|
this.link_id = new bson_objectid_1.default();
|
|
@@ -20,6 +21,7 @@ class LinkRedirect {
|
|
|
20
21
|
this.from = data.from;
|
|
21
22
|
this.to = data.to;
|
|
22
23
|
this.edited = !!data.edited;
|
|
24
|
+
this.automationActionRevisionId = data.automationActionRevisionId ?? undefined;
|
|
23
25
|
}
|
|
24
26
|
}
|
|
25
27
|
exports.LinkRedirect = LinkRedirect;
|
|
@@ -5,8 +5,9 @@ export class LinkRedirect {
|
|
|
5
5
|
from: URL;
|
|
6
6
|
to: URL;
|
|
7
7
|
edited: boolean;
|
|
8
|
+
automationActionRevisionId: string | undefined;
|
|
8
9
|
|
|
9
|
-
constructor(data: {id?: string; from: URL; to: URL; edited?: boolean}) {
|
|
10
|
+
constructor(data: {id?: string; from: URL; to: URL; edited?: boolean; automationActionRevisionId?: string | null}) {
|
|
10
11
|
if (!data.id) {
|
|
11
12
|
this.link_id = new ObjectID();
|
|
12
13
|
}
|
|
@@ -18,5 +19,6 @@ export class LinkRedirect {
|
|
|
18
19
|
this.from = data.from;
|
|
19
20
|
this.to = data.to;
|
|
20
21
|
this.edited = !!data.edited;
|
|
22
|
+
this.automationActionRevisionId = data.automationActionRevisionId ?? undefined;
|
|
21
23
|
}
|
|
22
24
|
}
|
|
@@ -6,6 +6,7 @@ const {LinkRedirect} = require('./link-redirect');
|
|
|
6
6
|
/**
|
|
7
7
|
* @typedef {object} ILinkRedirectRepository
|
|
8
8
|
* @prop {(url: URL) => Promise<LinkRedirect|undefined>} getByURL
|
|
9
|
+
* @prop {(automationActionRevisionId: string, url: URL) => Promise<LinkRedirect|undefined>} getByAutomationActionRevisionAndURL
|
|
9
10
|
* @prop {({filter: string}) => Promise<LinkRedirect[]>} getAll
|
|
10
11
|
* @prop {({filter: string}) => Promise<String[]>} getFilteredIds
|
|
11
12
|
* @prop {(linkRedirect: LinkRedirect) => Promise<void>} save
|
|
@@ -19,6 +20,8 @@ const MEMBER_UUID_PLACEHOLDER = '%%{uuid}%%';
|
|
|
19
20
|
// Ghost uses UUID v4; this regex is not that strict
|
|
20
21
|
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
21
22
|
|
|
23
|
+
const isUniqueConstraintError = err => err?.code === 'ER_DUP_ENTRY' || err?.code?.startsWith?.('SQLITE_CONSTRAINT');
|
|
24
|
+
|
|
22
25
|
class LinkRedirectsService {
|
|
23
26
|
/** @type ILinkRedirectRepository */
|
|
24
27
|
#linkRedirectRepository;
|
|
@@ -71,13 +74,15 @@ class LinkRedirectsService {
|
|
|
71
74
|
/**
|
|
72
75
|
* @param {URL} from
|
|
73
76
|
* @param {URL} to
|
|
77
|
+
* @param {string} [automationActionRevisionId]
|
|
74
78
|
*
|
|
75
79
|
* @returns {Promise<LinkRedirect>}
|
|
76
80
|
*/
|
|
77
|
-
async addRedirect(from, to) {
|
|
81
|
+
async addRedirect(from, to, automationActionRevisionId) {
|
|
78
82
|
const link = new LinkRedirect({
|
|
79
83
|
from,
|
|
80
|
-
to
|
|
84
|
+
to,
|
|
85
|
+
automationActionRevisionId
|
|
81
86
|
});
|
|
82
87
|
|
|
83
88
|
await this.#linkRedirectRepository.save(link);
|
|
@@ -85,6 +90,37 @@ class LinkRedirectsService {
|
|
|
85
90
|
return link;
|
|
86
91
|
}
|
|
87
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Get the shared redirect for a destination within an automation action revision,
|
|
95
|
+
* creating it on first use. A concurrent winning insert is returned after a
|
|
96
|
+
* duplicate-key failure.
|
|
97
|
+
* @param {string} automationActionRevisionId
|
|
98
|
+
* @param {Readonly<URL>} to
|
|
99
|
+
* @returns {Promise<LinkRedirect>}
|
|
100
|
+
*/
|
|
101
|
+
async getOrAddAutomationRedirect(automationActionRevisionId, to) {
|
|
102
|
+
const existing = await this.#linkRedirectRepository.getByAutomationActionRevisionAndURL(automationActionRevisionId, to);
|
|
103
|
+
if (existing) {
|
|
104
|
+
return existing;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const from = await this.getSlugUrl();
|
|
108
|
+
try {
|
|
109
|
+
return await this.addRedirect(from, to, automationActionRevisionId);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
if (!isUniqueConstraintError(err)) {
|
|
112
|
+
throw err;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const winner = await this.#linkRedirectRepository.getByAutomationActionRevisionAndURL(automationActionRevisionId, to);
|
|
116
|
+
if (winner) {
|
|
117
|
+
return winner;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
throw err;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
88
124
|
/**
|
|
89
125
|
* Returns the relative path prefix without subdirectory (e.g., /r/)
|
|
90
126
|
* Used for Express route matching where subdirectory is already stripped
|
|
@@ -7,6 +7,8 @@ module.exports = class ClickEvent {
|
|
|
7
7
|
member_uuid;
|
|
8
8
|
/** @type {ObjectID} */
|
|
9
9
|
link_id;
|
|
10
|
+
/** @type {Date | undefined} */
|
|
11
|
+
timestamp;
|
|
10
12
|
|
|
11
13
|
constructor(data) {
|
|
12
14
|
if (!data.id) {
|
|
@@ -21,5 +23,6 @@ module.exports = class ClickEvent {
|
|
|
21
23
|
|
|
22
24
|
this.member_uuid = data.member_uuid;
|
|
23
25
|
this.link_id = data.link_id;
|
|
26
|
+
this.timestamp = data.timestamp;
|
|
24
27
|
}
|
|
25
28
|
};
|
|
@@ -4,12 +4,37 @@ const errors = require('@tryghost/errors');
|
|
|
4
4
|
const urlUtils = require('../../../shared/url-utils').default;
|
|
5
5
|
|
|
6
6
|
class LinkTrackingServiceWrapper {
|
|
7
|
+
#initPromise;
|
|
8
|
+
#automationsApi;
|
|
9
|
+
|
|
10
|
+
constructor({
|
|
11
|
+
automationsApi = require('../automations/automations-api')
|
|
12
|
+
} = {}) {
|
|
13
|
+
this.#automationsApi = automationsApi;
|
|
14
|
+
}
|
|
15
|
+
|
|
7
16
|
async init() {
|
|
8
17
|
if (this.service) {
|
|
9
18
|
// Already done
|
|
10
19
|
return;
|
|
11
20
|
}
|
|
12
21
|
|
|
22
|
+
if (!this.#initPromise) {
|
|
23
|
+
this.#initPromise = this.#initialise();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const initPromise = this.#initPromise;
|
|
27
|
+
try {
|
|
28
|
+
await initPromise;
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (this.#initPromise === initPromise) {
|
|
31
|
+
this.#initPromise = undefined;
|
|
32
|
+
}
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async #initialise() {
|
|
13
38
|
const linkRedirection = require('../link-redirection');
|
|
14
39
|
if (!linkRedirection.service) {
|
|
15
40
|
throw new errors.InternalServerError({message: 'LinkRedirectionService should be initialised before LinkTrackingService'});
|
|
@@ -27,24 +52,31 @@ class LinkTrackingServiceWrapper {
|
|
|
27
52
|
linkRedirectRepository: linkRedirection.linkRedirectRepository
|
|
28
53
|
});
|
|
29
54
|
|
|
30
|
-
|
|
55
|
+
const linkClickRepository = new LinkClickRepository({
|
|
31
56
|
MemberLinkClickEventModel: models.MemberClickEvent,
|
|
32
57
|
Member: models.Member,
|
|
33
58
|
MemberLinkClickEvent: MemberLinkClickEvent,
|
|
34
59
|
DomainEvents
|
|
35
60
|
});
|
|
36
61
|
|
|
37
|
-
|
|
38
|
-
this.service = new LinkClickTrackingService({
|
|
62
|
+
const service = new LinkClickTrackingService({
|
|
39
63
|
linkRedirectService: linkRedirection.service,
|
|
40
|
-
linkClickRepository
|
|
64
|
+
linkClickRepository,
|
|
41
65
|
postLinkRepository,
|
|
42
66
|
DomainEvents,
|
|
43
|
-
urlUtils
|
|
67
|
+
urlUtils,
|
|
68
|
+
automationsApi: this.#automationsApi,
|
|
69
|
+
runInTransaction: callback => models.Base.transaction(callback)
|
|
44
70
|
});
|
|
45
71
|
|
|
46
|
-
await
|
|
72
|
+
await service.init();
|
|
73
|
+
|
|
74
|
+
// Expose the service only after it has finished initialising
|
|
75
|
+
this.linkClickRepository = linkClickRepository;
|
|
76
|
+
this.service = service;
|
|
47
77
|
}
|
|
48
78
|
}
|
|
49
79
|
|
|
50
80
|
module.exports = new LinkTrackingServiceWrapper();
|
|
81
|
+
// Exposed for testing purposes only
|
|
82
|
+
module.exports.LinkTrackingServiceWrapper = LinkTrackingServiceWrapper;
|
|
@@ -31,8 +31,13 @@ module.exports = class LinkClickRepository {
|
|
|
31
31
|
this.#DomainEvents = deps.DomainEvents;
|
|
32
32
|
|
|
33
33
|
// Memoize the findOne function
|
|
34
|
-
this.memoizedFindOne = _.memoize(async (uuid) => {
|
|
35
|
-
|
|
34
|
+
this.memoizedFindOne = _.memoize(async (uuid, options) => {
|
|
35
|
+
try {
|
|
36
|
+
return await this.#Member.findOne({uuid}, options);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
this.memoizedFindOne.cache.delete(uuid);
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
36
41
|
});
|
|
37
42
|
}
|
|
38
43
|
|
|
@@ -54,15 +59,18 @@ module.exports = class LinkClickRepository {
|
|
|
54
59
|
|
|
55
60
|
/**
|
|
56
61
|
* @param {LinkClick} linkClick
|
|
57
|
-
* @
|
|
62
|
+
* @param {object} [options]
|
|
63
|
+
* @param {object} [options.transacting]
|
|
64
|
+
* @returns {Promise<string | undefined>}
|
|
58
65
|
*/
|
|
59
|
-
async save(linkClick) {
|
|
66
|
+
async save(linkClick, options = {}) {
|
|
60
67
|
let member;
|
|
68
|
+
const memberLookupOptions = options.transacting ? {transacting: options.transacting} : undefined;
|
|
61
69
|
|
|
62
70
|
if (config && config.get('linkClickTrackingCacheMemberUuid')) {
|
|
63
|
-
member = await this.memoizedFindOne(linkClick.member_uuid);
|
|
71
|
+
member = await this.memoizedFindOne(linkClick.member_uuid, memberLookupOptions);
|
|
64
72
|
} else {
|
|
65
|
-
member = await this.#Member.findOne({uuid: linkClick.member_uuid});
|
|
73
|
+
member = await this.#Member.findOne({uuid: linkClick.member_uuid}, memberLookupOptions);
|
|
66
74
|
}
|
|
67
75
|
|
|
68
76
|
if (!member) {
|
|
@@ -76,11 +84,29 @@ module.exports = class LinkClickRepository {
|
|
|
76
84
|
// Only store the pathname (no support for variable query strings)
|
|
77
85
|
redirect_id: linkClick.link_id.toHexString(),
|
|
78
86
|
member_id: member.id
|
|
79
|
-
},
|
|
87
|
+
}, options);
|
|
80
88
|
|
|
81
89
|
linkClick.event_id = ObjectID.createFromHexString(model.id);
|
|
90
|
+
const timestamp = linkClick.timestamp ?? new Date();
|
|
91
|
+
|
|
92
|
+
const event = this.#MemberLinkClickEvent.create({
|
|
93
|
+
memberId: member.id,
|
|
94
|
+
memberLastSeenAt: member.get('last_seen_at'),
|
|
95
|
+
linkId: linkClick.link_id.toHexString()
|
|
96
|
+
}, timestamp);
|
|
97
|
+
|
|
98
|
+
if (options.transacting) {
|
|
99
|
+
options.transacting.executionPromise.then(() => {
|
|
100
|
+
try {
|
|
101
|
+
this.#DomainEvents.dispatch(event);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
sentry.captureException(error);
|
|
104
|
+
}
|
|
105
|
+
}, () => undefined);
|
|
106
|
+
} else {
|
|
107
|
+
this.#DomainEvents.dispatch(event);
|
|
108
|
+
}
|
|
82
109
|
|
|
83
|
-
|
|
84
|
-
this.#DomainEvents.dispatch(this.#MemberLinkClickEvent.create({memberId: member.id, memberLastSeenAt: member.get('last_seen_at'), linkId: linkClick.link_id.toHexString()}, new Date()));
|
|
110
|
+
return member.id;
|
|
85
111
|
}
|
|
86
112
|
};
|
|
@@ -4,13 +4,14 @@ const PostLink = require('./post-link');
|
|
|
4
4
|
const ObjectID = require('bson-objectid').default;
|
|
5
5
|
const errors = require('@tryghost/errors');
|
|
6
6
|
const nql = require('@tryghost/nql');
|
|
7
|
+
const {escapeNqlString} = require('@tryghost/nql-string');
|
|
7
8
|
const _ = require('lodash');
|
|
8
9
|
const tpl = require('@tryghost/tpl');
|
|
9
10
|
const moment = require('moment');
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* @typedef {object} ILinkClickRepository
|
|
13
|
-
* @prop {(event: LinkClick) => Promise<
|
|
14
|
+
* @prop {(event: LinkClick, options?: {transacting?: object}) => Promise<string | undefined>} save
|
|
14
15
|
* @prop {({filter: string}) => Promise<LinkClick[]>} getAll
|
|
15
16
|
*/
|
|
16
17
|
|
|
@@ -61,6 +62,10 @@ class LinkClickTrackingService {
|
|
|
61
62
|
#LinkRedirect;
|
|
62
63
|
/** @type {Object} */
|
|
63
64
|
#urlUtils;
|
|
65
|
+
/** @type {object} */
|
|
66
|
+
#automationsApi;
|
|
67
|
+
/** @type {(callback: (transacting: object) => Promise<void>) => Promise<void>} */
|
|
68
|
+
#runInTransaction;
|
|
64
69
|
|
|
65
70
|
/**
|
|
66
71
|
* @param {object} deps
|
|
@@ -69,6 +74,8 @@ class LinkClickTrackingService {
|
|
|
69
74
|
* @param {IPostLinkRepository} deps.postLinkRepository
|
|
70
75
|
* @param {DomainEvents} deps.DomainEvents
|
|
71
76
|
* @param {urlUtils} deps.urlUtils
|
|
77
|
+
* @param {object} deps.automationsApi
|
|
78
|
+
* @param {(callback: (transacting: object) => Promise<void>) => Promise<void>} deps.runInTransaction
|
|
72
79
|
*/
|
|
73
80
|
constructor(deps) {
|
|
74
81
|
this.#linkClickRepository = deps.linkClickRepository;
|
|
@@ -76,6 +83,8 @@ class LinkClickTrackingService {
|
|
|
76
83
|
this.#postLinkRepository = deps.postLinkRepository;
|
|
77
84
|
this.#DomainEvents = deps.DomainEvents;
|
|
78
85
|
this.#urlUtils = deps.urlUtils;
|
|
86
|
+
this.#automationsApi = deps.automationsApi;
|
|
87
|
+
this.#runInTransaction = deps.runInTransaction;
|
|
79
88
|
}
|
|
80
89
|
|
|
81
90
|
async init() {
|
|
@@ -166,7 +175,11 @@ class LinkClickTrackingService {
|
|
|
166
175
|
|
|
167
176
|
// manages transformation of current url to relative for comparision
|
|
168
177
|
const transformedOldUrl = this.#urlUtils.absoluteToTransformReady(redirectUrl.href);
|
|
169
|
-
|
|
178
|
+
// the url is re-serialised into NQL here, so it needs escaping again -
|
|
179
|
+
// quotes are legal in a URL and would otherwise break the filter.
|
|
180
|
+
// postId is String()d because nql parses an unquoted `post_id:123` to a
|
|
181
|
+
// number, and escapeNqlString operates on strings
|
|
182
|
+
const filterQuery = `post_id:${escapeNqlString(String(postId))}+to:${escapeNqlString(transformedOldUrl)}`;
|
|
170
183
|
|
|
171
184
|
const updatedFilterOptions = {
|
|
172
185
|
...filterOptions,
|
|
@@ -233,6 +246,22 @@ class LinkClickTrackingService {
|
|
|
233
246
|
return url;
|
|
234
247
|
}
|
|
235
248
|
|
|
249
|
+
/**
|
|
250
|
+
* Add first-party tracking to a URL in an automation email
|
|
251
|
+
* @param {Readonly<URL>} url
|
|
252
|
+
* @param {string} automationActionRevisionId
|
|
253
|
+
* @param {string} automationRunStepId
|
|
254
|
+
* @param {string} memberUuid
|
|
255
|
+
* @return {Promise<URL>}
|
|
256
|
+
*/
|
|
257
|
+
async addAutomationTrackingToUrl(url, automationActionRevisionId, automationRunStepId, memberUuid) {
|
|
258
|
+
const redirect = await this.#linkRedirectService.getOrAddAutomationRedirect(automationActionRevisionId, url);
|
|
259
|
+
const trackedUrl = new URL(redirect.from.href);
|
|
260
|
+
trackedUrl.searchParams.set('m', memberUuid);
|
|
261
|
+
trackedUrl.searchParams.set('step', automationRunStepId);
|
|
262
|
+
return trackedUrl;
|
|
263
|
+
}
|
|
264
|
+
|
|
236
265
|
subscribe() {
|
|
237
266
|
this.#DomainEvents.subscribe(RedirectEvent, async (event) => {
|
|
238
267
|
const uuid = event.data.url.searchParams.get('m');
|
|
@@ -242,9 +271,30 @@ class LinkClickTrackingService {
|
|
|
242
271
|
|
|
243
272
|
const click = new LinkClick({
|
|
244
273
|
member_uuid: uuid,
|
|
245
|
-
link_id: event.data.link.link_id
|
|
274
|
+
link_id: event.data.link.link_id,
|
|
275
|
+
timestamp: event.timestamp
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const automationActionRevisionId = event.data.link.automationActionRevisionId;
|
|
279
|
+
const automationRunStepId = event.data.url.searchParams.get('step');
|
|
280
|
+
if (!automationActionRevisionId || !automationRunStepId) {
|
|
281
|
+
await this.#linkClickRepository.save(click);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
await this.#runInTransaction(async (transacting) => {
|
|
286
|
+
const memberId = await this.#linkClickRepository.save(click, {transacting});
|
|
287
|
+
if (!memberId) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
await this.#automationsApi.trackEmailClicked({
|
|
292
|
+
automationActionRevisionId,
|
|
293
|
+
automationRunStepId,
|
|
294
|
+
memberId,
|
|
295
|
+
clickedAt: event.timestamp
|
|
296
|
+
}, {transacting});
|
|
246
297
|
});
|
|
247
|
-
await this.#linkClickRepository.save(click);
|
|
248
298
|
});
|
|
249
299
|
}
|
|
250
300
|
}
|
|
@@ -11,7 +11,9 @@ const blockedReferrerDomains = [
|
|
|
11
11
|
'www.chicagomag.com',
|
|
12
12
|
'bailii.org',
|
|
13
13
|
'www.bailii.org',
|
|
14
|
-
'*.doubleclick.net'
|
|
14
|
+
'*.doubleclick.net',
|
|
15
|
+
// Substack's client app throws on any non-empty ?ref= value and swaps the post for an error screen
|
|
16
|
+
'*.substack.com'
|
|
15
17
|
];
|
|
16
18
|
|
|
17
19
|
const isBlockedReferrerDomain = (domain) => {
|
|
@@ -2,11 +2,13 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const lexicalLib = require('../../lib/lexical');
|
|
4
4
|
const labs = require('../../../shared/labs');
|
|
5
|
+
const urlUtils = require('../../../shared/url-utils').default;
|
|
5
6
|
const {finalize} = require('../email-rendering/finalize');
|
|
6
7
|
const errors = require('@tryghost/errors');
|
|
7
8
|
const {MESSAGES} = require('./constants');
|
|
8
9
|
const {wrapReplacementStrings} = require('@tryghost/kg-default-nodes').utils.replacementStrings;
|
|
9
10
|
const linkReplacer = require('../lib/link-replacer');
|
|
11
|
+
const linkTracking = require('../link-tracking');
|
|
10
12
|
const emailDesign = require('../email-rendering/email-design');
|
|
11
13
|
const {registerHelpers} = require('../email-service/helpers/register-helpers');
|
|
12
14
|
|
|
@@ -115,9 +117,12 @@ class MemberWelcomeEmailRenderer {
|
|
|
115
117
|
* @param {Object} options.member - Member data (name, email)
|
|
116
118
|
* @param {Object} options.siteSettings - Site settings (title, url, accentColor)
|
|
117
119
|
* @param {string} [options.unsubscribeUrl] - When set, the footer shows an "Unsubscribe from these emails" link instead of "Manage your preferences"
|
|
120
|
+
* @param {boolean} [options.trackClicks]
|
|
121
|
+
* @param {string | null} [options.automationActionRevisionId]
|
|
122
|
+
* @param {string | null} [options.automationRunStepId]
|
|
118
123
|
* @returns {Promise<{html: string, text: string, subject: string}>}
|
|
119
124
|
*/
|
|
120
|
-
async render({lexical, subject, designSettings, member, siteSettings, unsubscribeUrl}) {
|
|
125
|
+
async render({lexical, subject, designSettings, member, siteSettings, unsubscribeUrl, trackClicks = false, automationActionRevisionId = null, automationRunStepId = null}) {
|
|
121
126
|
designSettings = designSettings || {};
|
|
122
127
|
|
|
123
128
|
const design = emailDesign.getEmailDesign({
|
|
@@ -138,7 +143,8 @@ class MemberWelcomeEmailRenderer {
|
|
|
138
143
|
|
|
139
144
|
let content;
|
|
140
145
|
try {
|
|
141
|
-
|
|
146
|
+
const absoluteLexical = urlUtils.transformReadyToAbsolute(lexical);
|
|
147
|
+
content = await lexicalLib.render(absoluteLexical, {target: 'email', design});
|
|
142
148
|
} catch (err) {
|
|
143
149
|
throw new errors.IncorrectUsageError({
|
|
144
150
|
message: MESSAGES.INVALID_LEXICAL_STRUCTURE,
|
|
@@ -158,8 +164,18 @@ class MemberWelcomeEmailRenderer {
|
|
|
158
164
|
const contentWithReplacements = this.#applyReplacements({definitions, text: content, escapeHtml: true});
|
|
159
165
|
const subjectWithReplacements = this.#applyReplacements({definitions, text: subject, escapeHtml: false});
|
|
160
166
|
|
|
161
|
-
// Resolve relative links
|
|
162
|
-
|
|
167
|
+
// Resolve relative links, including Ghost Portal routes such as #/portal/signup,
|
|
168
|
+
// while preserving document-local anchors such as #section.
|
|
169
|
+
const contentWithAbsoluteLinks = await linkReplacer.replace(contentWithReplacements, async (url, originalPath) => {
|
|
170
|
+
const isDocumentAnchor = originalPath.startsWith('#') && !originalPath.startsWith('#/');
|
|
171
|
+
if (isDocumentAnchor) {
|
|
172
|
+
return originalPath;
|
|
173
|
+
}
|
|
174
|
+
const isTrackable = ['http:', 'https:'].includes(url.protocol);
|
|
175
|
+
if (trackClicks && automationActionRevisionId && automationRunStepId && member.uuid && isTrackable) {
|
|
176
|
+
await linkTracking.init();
|
|
177
|
+
return await linkTracking.service.addAutomationTrackingToUrl(url, automationActionRevisionId, automationRunStepId, member.uuid);
|
|
178
|
+
}
|
|
163
179
|
return url;
|
|
164
180
|
}, {base: siteSettings.url});
|
|
165
181
|
|