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
|
@@ -21,6 +21,21 @@ const MAX_KEY_LENGTH = 191;
|
|
|
21
21
|
// validated by its own field type's schema, which isn't known until the key is
|
|
22
22
|
// resolved to a definition.
|
|
23
23
|
const ValuesInput = zod_1.z.record(zod_1.z.string().max(MAX_KEY_LENGTH), zod_1.z.unknown());
|
|
24
|
+
// JSON.parse as a total function. Anything unreadable — unparseable text, or the null
|
|
25
|
+
// a nullable column can always return — reads as `undefined`, which no value schema
|
|
26
|
+
// accepts. That way a corrupt blob is turned away by the same check that turns away a
|
|
27
|
+
// well-formed one of the wrong shape, instead of needing a branch of its own.
|
|
28
|
+
function decodeJson(raw) {
|
|
29
|
+
if (raw === null) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(raw);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
24
39
|
/**
|
|
25
40
|
* The value half of member custom fields: reading and writing what a member holds
|
|
26
41
|
* for each defined field. It belongs to the member aggregate — a value is data
|
|
@@ -137,8 +152,9 @@ class CustomFieldValuesService {
|
|
|
137
152
|
/**
|
|
138
153
|
* Resolve input into the writes it implies, rejecting anything invalid, and
|
|
139
154
|
* writing nothing. Returned so a caller can validate before it commits to a
|
|
140
|
-
* change it would have to unwind (the member edit
|
|
141
|
-
* apply the same plan without
|
|
155
|
+
* change it would have to unwind (the member edit and the importer both validate
|
|
156
|
+
* up front, before opening a transaction), then apply the same plan without
|
|
157
|
+
* re-resolving or re-validating.
|
|
142
158
|
*/
|
|
143
159
|
async planWrite(input) {
|
|
144
160
|
const values = this.parseValues(input);
|
|
@@ -187,25 +203,129 @@ class CustomFieldValuesService {
|
|
|
187
203
|
}
|
|
188
204
|
return writes;
|
|
189
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* @private
|
|
208
|
+
* The composite values this member already holds for the fields in a plan,
|
|
209
|
+
* keyed by field id. Composites only: a scalar has nothing to merge into.
|
|
210
|
+
*
|
|
211
|
+
* Read through the caller's executor, never the base connection. On a
|
|
212
|
+
* single-connection pool a read off `this.knex` inside an open transaction
|
|
213
|
+
* waits for the connection that transaction is holding, and hangs.
|
|
214
|
+
*
|
|
215
|
+
* A blob that won't decode is left out, so the write replaces it rather than
|
|
216
|
+
* merging into something unreadable.
|
|
217
|
+
*/
|
|
218
|
+
async storedComposites(trx, memberId, writes) {
|
|
219
|
+
const composites = new Map();
|
|
220
|
+
// Merging is only meaningful for a value with parts, which is a fact about the
|
|
221
|
+
// type's shape rather than the column it lands in. Asking the storage type
|
|
222
|
+
// instead would sweep in the first json-backed type that isn't a record — a
|
|
223
|
+
// multi-select held as a list, say — and log a decode failure for every row of
|
|
224
|
+
// an import, over a value that reads back perfectly well.
|
|
225
|
+
const fieldIds = writes
|
|
226
|
+
.filter(({ field, value }) => value !== undefined && (0, custom_field_types_1.subFieldsOf)(field.type) !== null)
|
|
227
|
+
.map(({ field }) => field.id);
|
|
228
|
+
if (fieldIds.length === 0) {
|
|
229
|
+
return composites;
|
|
230
|
+
}
|
|
231
|
+
const rows = await trx(VALUES_TABLE)
|
|
232
|
+
.where('member_id', memberId)
|
|
233
|
+
.whereIn('custom_field_id', fieldIds)
|
|
234
|
+
.whereNotNull('value_json')
|
|
235
|
+
.select('custom_field_id', 'value_json')
|
|
236
|
+
// Merging reads a value and writes it back, so the row has to stay still in
|
|
237
|
+
// between: without the lock a write committing after this read is overwritten
|
|
238
|
+
// by the sub-fields it just replaced. knex omits this on SQLite, which needs
|
|
239
|
+
// no lock — its pool is a single connection, so writes never overlap.
|
|
240
|
+
.forUpdate();
|
|
241
|
+
for (const row of rows) {
|
|
242
|
+
// The blob is the edge here: the columns come from a query written just
|
|
243
|
+
// above, but whatever an earlier write left in value_json is unknown until
|
|
244
|
+
// it is read back. Parsing it is what narrows the type, and it rejects the
|
|
245
|
+
// array, null or bare number the column could equally be holding.
|
|
246
|
+
const stored = schema_1.StoredCompositeValue.safeParse(decodeJson(row.value_json));
|
|
247
|
+
if (stored.success) {
|
|
248
|
+
composites.set(row.custom_field_id, stored.data);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
logging_1.default.warn(`Replacing an unreadable custom field value rather than merging into it (member '${memberId}', field '${row.custom_field_id}').`);
|
|
252
|
+
}
|
|
253
|
+
return composites;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* @private
|
|
257
|
+
* The stored sub-fields with the incoming ones written over them, re-checked
|
|
258
|
+
* against the field type before it is handed back.
|
|
259
|
+
*
|
|
260
|
+
* The re-check is what makes this safe to write without a second thought. Both
|
|
261
|
+
* halves were valid when they were written, but "valid" is a moving target: the
|
|
262
|
+
* stored half may predate a change to the type, so it can carry a sub-field the
|
|
263
|
+
* type has since dropped or a bound it has since tightened. Parsing the result
|
|
264
|
+
* strips what no longer belongs instead of faithfully persisting it on every
|
|
265
|
+
* import from here on.
|
|
266
|
+
*
|
|
267
|
+
* A merged value that won't parse falls back to the incoming value alone rather
|
|
268
|
+
* than failing. The row is trying to write something valid, and refusing it over
|
|
269
|
+
* data that was already in the database punishes the wrong write.
|
|
270
|
+
*/
|
|
271
|
+
mergeComposite(field, existing, incoming) {
|
|
272
|
+
const merged = custom_field_types_1.FIELD_TYPES[field.type].value.safeParse({ ...existing, ...incoming });
|
|
273
|
+
if (merged.success) {
|
|
274
|
+
return merged.data;
|
|
275
|
+
}
|
|
276
|
+
logging_1.default.warn(`Replacing a custom field value that no longer validates rather than merging into it (field '${field.key}').`);
|
|
277
|
+
return incoming;
|
|
278
|
+
}
|
|
190
279
|
/**
|
|
191
280
|
* Apply a plan from `planWrite`.
|
|
192
281
|
*
|
|
193
282
|
* Merge, not replace: only the fields in the plan are touched, so a caller
|
|
194
|
-
* that doesn't know about a field can't erase it.
|
|
195
|
-
*
|
|
283
|
+
* that doesn't know about a field can't erase it.
|
|
284
|
+
*
|
|
285
|
+
* `mergeComposites` extends that same rule from a field to a composite's
|
|
286
|
+
* sub-fields, because a sub-field is a field: a row writes the sub-fields it
|
|
287
|
+
* fills and leaves the rest alone, exactly as a blank `name` column leaves a
|
|
288
|
+
* member's name alone. The CSV import needs it — without it a file naming one
|
|
289
|
+
* address column would replace the whole address, clearing five sub-fields it
|
|
290
|
+
* never mentioned. The API does not merge: a PUT sends a whole address, so
|
|
291
|
+
* what it sends is what should be stored.
|
|
292
|
+
*
|
|
293
|
+
* The merge is asked for here rather than at plan time, where the caller's intent
|
|
294
|
+
* is otherwise expressed, because it cannot happen before a transaction is open:
|
|
295
|
+
* it reads the stored value and writes it back, and the row lock that keeps those
|
|
296
|
+
* two steps together only exists inside the transaction. `planWrite` runs before
|
|
297
|
+
* one is opened -- the importer plans a row, then opens a transaction per member --
|
|
298
|
+
* so a merge decided there would have to read outside the lock it depends on.
|
|
299
|
+
*
|
|
300
|
+
* Always transactional, so a mid-batch failure rolls the batch back. Passed an
|
|
301
|
+
* executor it joins that transaction -- the importer passes its per-member one, so a
|
|
302
|
+
* failed value write takes the member with it; passed nothing it opens its own.
|
|
196
303
|
*/
|
|
197
|
-
async applyWrite(memberId, writes) {
|
|
304
|
+
async applyWrite(memberId, writes, executor = this.knex, { mergeComposites = false } = {}) {
|
|
198
305
|
if (writes.length === 0) {
|
|
199
306
|
return;
|
|
200
307
|
}
|
|
201
|
-
|
|
308
|
+
const apply = async (trx) => {
|
|
309
|
+
const stored = mergeComposites
|
|
310
|
+
? await this.storedComposites(trx, memberId, writes)
|
|
311
|
+
: new Map();
|
|
202
312
|
for (const { field, value } of writes) {
|
|
203
313
|
const target = { member_id: memberId, custom_field_id: field.id };
|
|
204
314
|
if (value === undefined) {
|
|
205
315
|
await trx(VALUES_TABLE).where(target).del();
|
|
206
316
|
continue;
|
|
207
317
|
}
|
|
208
|
-
|
|
318
|
+
// Both sides of the merge go through the same shape check, so it joins
|
|
319
|
+
// two objects the type system knows about rather than two assertions.
|
|
320
|
+
// The plan already validated `value` against its field type, so for a
|
|
321
|
+
// composite this parse cannot fail; a scalar simply doesn't match, and
|
|
322
|
+
// falls through to being written as it stands.
|
|
323
|
+
const existing = stored.get(field.id);
|
|
324
|
+
const incoming = schema_1.StoredCompositeValue.safeParse(value);
|
|
325
|
+
const merged = existing && incoming.success
|
|
326
|
+
? this.mergeComposite(field, existing, incoming.data)
|
|
327
|
+
: value;
|
|
328
|
+
const valueColumns = (0, storage_1.storageColumnsFor)(field.type, merged);
|
|
209
329
|
await trx(VALUES_TABLE)
|
|
210
330
|
.insert({ id: new bson_objectid_1.default().toHexString(), ...target, ...valueColumns, created_at: new Date() })
|
|
211
331
|
// The unique index on (member_id, custom_field_id) is
|
|
@@ -213,7 +333,15 @@ class CustomFieldValuesService {
|
|
|
213
333
|
.onConflict(['member_id', 'custom_field_id'])
|
|
214
334
|
.merge({ ...valueColumns, updated_at: new Date() });
|
|
215
335
|
}
|
|
216
|
-
}
|
|
336
|
+
};
|
|
337
|
+
// isTransaction is knex's marker for a transactor: join an existing transaction
|
|
338
|
+
// rather than nesting a savepoint under it, otherwise open one.
|
|
339
|
+
if (executor.isTransaction) {
|
|
340
|
+
await apply(executor);
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
await executor.transaction(apply);
|
|
344
|
+
}
|
|
217
345
|
}
|
|
218
346
|
}
|
|
219
347
|
exports.CustomFieldValuesService = CustomFieldValuesService;
|
|
@@ -3,8 +3,8 @@ import errors from '@tryghost/errors';
|
|
|
3
3
|
import logging from '@tryghost/logging';
|
|
4
4
|
import type {Knex} from 'knex';
|
|
5
5
|
import {z} from 'zod';
|
|
6
|
-
import {FIELD_TYPES, type FieldType} from '@tryghost/custom-field-types';
|
|
7
|
-
import {DbCustomFieldValueWithField, FIELD_STATUS} from './schema';
|
|
6
|
+
import {FIELD_TYPES, subFieldsOf, type FieldType} from '@tryghost/custom-field-types';
|
|
7
|
+
import {DbCustomFieldValueWithField, FIELD_STATUS, StoredCompositeValue} from './schema';
|
|
8
8
|
import {activeFields} from './queries';
|
|
9
9
|
import {storageCodecFor, storageColumnsFor} from './storage';
|
|
10
10
|
|
|
@@ -20,6 +20,21 @@ const MAX_KEY_LENGTH = 191;
|
|
|
20
20
|
// resolved to a definition.
|
|
21
21
|
const ValuesInput = z.record(z.string().max(MAX_KEY_LENGTH), z.unknown());
|
|
22
22
|
|
|
23
|
+
// JSON.parse as a total function. Anything unreadable — unparseable text, or the null
|
|
24
|
+
// a nullable column can always return — reads as `undefined`, which no value schema
|
|
25
|
+
// accepts. That way a corrupt blob is turned away by the same check that turns away a
|
|
26
|
+
// well-formed one of the wrong shape, instead of needing a branch of its own.
|
|
27
|
+
function decodeJson(raw: string | null): unknown {
|
|
28
|
+
if (raw === null) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(raw);
|
|
33
|
+
} catch {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
23
38
|
// The field facts the value path needs: the id to write the FK, the key to match
|
|
24
39
|
// input against, the name for error messages, and the type to pick the validator
|
|
25
40
|
// and storage column. All columns on members_custom_fields, so a value operation
|
|
@@ -164,8 +179,9 @@ export class CustomFieldValuesService {
|
|
|
164
179
|
/**
|
|
165
180
|
* Resolve input into the writes it implies, rejecting anything invalid, and
|
|
166
181
|
* writing nothing. Returned so a caller can validate before it commits to a
|
|
167
|
-
* change it would have to unwind (the member edit
|
|
168
|
-
* apply the same plan without
|
|
182
|
+
* change it would have to unwind (the member edit and the importer both validate
|
|
183
|
+
* up front, before opening a transaction), then apply the same plan without
|
|
184
|
+
* re-resolving or re-validating.
|
|
169
185
|
*/
|
|
170
186
|
async planWrite(input: unknown): Promise<PlannedWrite[]> {
|
|
171
187
|
const values = this.parseValues(input);
|
|
@@ -221,19 +237,126 @@ export class CustomFieldValuesService {
|
|
|
221
237
|
return writes;
|
|
222
238
|
}
|
|
223
239
|
|
|
240
|
+
/**
|
|
241
|
+
* @private
|
|
242
|
+
* The composite values this member already holds for the fields in a plan,
|
|
243
|
+
* keyed by field id. Composites only: a scalar has nothing to merge into.
|
|
244
|
+
*
|
|
245
|
+
* Read through the caller's executor, never the base connection. On a
|
|
246
|
+
* single-connection pool a read off `this.knex` inside an open transaction
|
|
247
|
+
* waits for the connection that transaction is holding, and hangs.
|
|
248
|
+
*
|
|
249
|
+
* A blob that won't decode is left out, so the write replaces it rather than
|
|
250
|
+
* merging into something unreadable.
|
|
251
|
+
*/
|
|
252
|
+
private async storedComposites(trx: Knex, memberId: string, writes: PlannedWrite[]): Promise<Map<string, Record<string, unknown>>> {
|
|
253
|
+
const composites = new Map<string, Record<string, unknown>>();
|
|
254
|
+
// Merging is only meaningful for a value with parts, which is a fact about the
|
|
255
|
+
// type's shape rather than the column it lands in. Asking the storage type
|
|
256
|
+
// instead would sweep in the first json-backed type that isn't a record — a
|
|
257
|
+
// multi-select held as a list, say — and log a decode failure for every row of
|
|
258
|
+
// an import, over a value that reads back perfectly well.
|
|
259
|
+
const fieldIds = writes
|
|
260
|
+
.filter(({field, value}) => value !== undefined && subFieldsOf(field.type) !== null)
|
|
261
|
+
.map(({field}) => field.id);
|
|
262
|
+
|
|
263
|
+
if (fieldIds.length === 0) {
|
|
264
|
+
return composites;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const rows = await trx(VALUES_TABLE)
|
|
268
|
+
.where('member_id', memberId)
|
|
269
|
+
.whereIn('custom_field_id', fieldIds)
|
|
270
|
+
.whereNotNull('value_json')
|
|
271
|
+
.select('custom_field_id', 'value_json')
|
|
272
|
+
// Merging reads a value and writes it back, so the row has to stay still in
|
|
273
|
+
// between: without the lock a write committing after this read is overwritten
|
|
274
|
+
// by the sub-fields it just replaced. knex omits this on SQLite, which needs
|
|
275
|
+
// no lock — its pool is a single connection, so writes never overlap.
|
|
276
|
+
.forUpdate();
|
|
277
|
+
|
|
278
|
+
for (const row of rows) {
|
|
279
|
+
// The blob is the edge here: the columns come from a query written just
|
|
280
|
+
// above, but whatever an earlier write left in value_json is unknown until
|
|
281
|
+
// it is read back. Parsing it is what narrows the type, and it rejects the
|
|
282
|
+
// array, null or bare number the column could equally be holding.
|
|
283
|
+
const stored = StoredCompositeValue.safeParse(decodeJson(row.value_json));
|
|
284
|
+
if (stored.success) {
|
|
285
|
+
composites.set(row.custom_field_id, stored.data);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
logging.warn(`Replacing an unreadable custom field value rather than merging into it (member '${memberId}', field '${row.custom_field_id}').`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return composites;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* @private
|
|
296
|
+
* The stored sub-fields with the incoming ones written over them, re-checked
|
|
297
|
+
* against the field type before it is handed back.
|
|
298
|
+
*
|
|
299
|
+
* The re-check is what makes this safe to write without a second thought. Both
|
|
300
|
+
* halves were valid when they were written, but "valid" is a moving target: the
|
|
301
|
+
* stored half may predate a change to the type, so it can carry a sub-field the
|
|
302
|
+
* type has since dropped or a bound it has since tightened. Parsing the result
|
|
303
|
+
* strips what no longer belongs instead of faithfully persisting it on every
|
|
304
|
+
* import from here on.
|
|
305
|
+
*
|
|
306
|
+
* A merged value that won't parse falls back to the incoming value alone rather
|
|
307
|
+
* than failing. The row is trying to write something valid, and refusing it over
|
|
308
|
+
* data that was already in the database punishes the wrong write.
|
|
309
|
+
*/
|
|
310
|
+
private mergeComposite(field: ActiveField, existing: Record<string, unknown>, incoming: Record<string, unknown>): unknown {
|
|
311
|
+
const merged = FIELD_TYPES[field.type].value.safeParse({...existing, ...incoming});
|
|
312
|
+
if (merged.success) {
|
|
313
|
+
return merged.data;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
logging.warn(`Replacing a custom field value that no longer validates rather than merging into it (field '${field.key}').`);
|
|
317
|
+
return incoming;
|
|
318
|
+
}
|
|
319
|
+
|
|
224
320
|
/**
|
|
225
321
|
* Apply a plan from `planWrite`.
|
|
226
322
|
*
|
|
227
323
|
* Merge, not replace: only the fields in the plan are touched, so a caller
|
|
228
|
-
* that doesn't know about a field can't erase it.
|
|
229
|
-
*
|
|
324
|
+
* that doesn't know about a field can't erase it.
|
|
325
|
+
*
|
|
326
|
+
* `mergeComposites` extends that same rule from a field to a composite's
|
|
327
|
+
* sub-fields, because a sub-field is a field: a row writes the sub-fields it
|
|
328
|
+
* fills and leaves the rest alone, exactly as a blank `name` column leaves a
|
|
329
|
+
* member's name alone. The CSV import needs it — without it a file naming one
|
|
330
|
+
* address column would replace the whole address, clearing five sub-fields it
|
|
331
|
+
* never mentioned. The API does not merge: a PUT sends a whole address, so
|
|
332
|
+
* what it sends is what should be stored.
|
|
333
|
+
*
|
|
334
|
+
* The merge is asked for here rather than at plan time, where the caller's intent
|
|
335
|
+
* is otherwise expressed, because it cannot happen before a transaction is open:
|
|
336
|
+
* it reads the stored value and writes it back, and the row lock that keeps those
|
|
337
|
+
* two steps together only exists inside the transaction. `planWrite` runs before
|
|
338
|
+
* one is opened -- the importer plans a row, then opens a transaction per member --
|
|
339
|
+
* so a merge decided there would have to read outside the lock it depends on.
|
|
340
|
+
*
|
|
341
|
+
* Always transactional, so a mid-batch failure rolls the batch back. Passed an
|
|
342
|
+
* executor it joins that transaction -- the importer passes its per-member one, so a
|
|
343
|
+
* failed value write takes the member with it; passed nothing it opens its own.
|
|
230
344
|
*/
|
|
231
|
-
async applyWrite(
|
|
345
|
+
async applyWrite(
|
|
346
|
+
memberId: string,
|
|
347
|
+
writes: PlannedWrite[],
|
|
348
|
+
executor: Knex = this.knex,
|
|
349
|
+
{mergeComposites = false}: {mergeComposites?: boolean} = {}
|
|
350
|
+
): Promise<void> {
|
|
232
351
|
if (writes.length === 0) {
|
|
233
352
|
return;
|
|
234
353
|
}
|
|
235
354
|
|
|
236
|
-
|
|
355
|
+
const apply = async (trx: Knex) => {
|
|
356
|
+
const stored = mergeComposites
|
|
357
|
+
? await this.storedComposites(trx, memberId, writes)
|
|
358
|
+
: new Map<string, Record<string, unknown>>();
|
|
359
|
+
|
|
237
360
|
for (const {field, value} of writes) {
|
|
238
361
|
const target = {member_id: memberId, custom_field_id: field.id};
|
|
239
362
|
|
|
@@ -242,7 +365,18 @@ export class CustomFieldValuesService {
|
|
|
242
365
|
continue;
|
|
243
366
|
}
|
|
244
367
|
|
|
245
|
-
|
|
368
|
+
// Both sides of the merge go through the same shape check, so it joins
|
|
369
|
+
// two objects the type system knows about rather than two assertions.
|
|
370
|
+
// The plan already validated `value` against its field type, so for a
|
|
371
|
+
// composite this parse cannot fail; a scalar simply doesn't match, and
|
|
372
|
+
// falls through to being written as it stands.
|
|
373
|
+
const existing = stored.get(field.id);
|
|
374
|
+
const incoming = StoredCompositeValue.safeParse(value);
|
|
375
|
+
const merged = existing && incoming.success
|
|
376
|
+
? this.mergeComposite(field, existing, incoming.data)
|
|
377
|
+
: value;
|
|
378
|
+
|
|
379
|
+
const valueColumns = storageColumnsFor(field.type, merged);
|
|
246
380
|
await trx(VALUES_TABLE)
|
|
247
381
|
.insert({id: new ObjectID().toHexString(), ...target, ...valueColumns, created_at: new Date()})
|
|
248
382
|
// The unique index on (member_id, custom_field_id) is
|
|
@@ -250,6 +384,14 @@ export class CustomFieldValuesService {
|
|
|
250
384
|
.onConflict(['member_id', 'custom_field_id'])
|
|
251
385
|
.merge({...valueColumns, updated_at: new Date()});
|
|
252
386
|
}
|
|
253
|
-
}
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
// isTransaction is knex's marker for a transactor: join an existing transaction
|
|
390
|
+
// rather than nesting a savepoint under it, otherwise open one.
|
|
391
|
+
if (executor.isTransaction) {
|
|
392
|
+
await apply(executor);
|
|
393
|
+
} else {
|
|
394
|
+
await executor.transaction(apply);
|
|
395
|
+
}
|
|
254
396
|
}
|
|
255
397
|
}
|
|
@@ -29,7 +29,11 @@ async function getPostData(post) {
|
|
|
29
29
|
|
|
30
30
|
function getPostUrl(id, postData) {
|
|
31
31
|
const jsonModel = {...postData};
|
|
32
|
-
|
|
32
|
+
// The URL service routes by resource type. Pages and posts share the Post
|
|
33
|
+
// model, so the page's own type must reach forPost — otherwise it defaults
|
|
34
|
+
// to 'posts', matches no post collection, and 404s under the lazy service.
|
|
35
|
+
const type = postData.type === 'page' ? 'pages' : 'posts';
|
|
36
|
+
outputSerializerUrlUtil.forPost(id, jsonModel, {options: {}}, type);
|
|
33
37
|
return jsonModel.url;
|
|
34
38
|
}
|
|
35
39
|
|
|
@@ -9,6 +9,7 @@ const charset = require('charset');
|
|
|
9
9
|
const iconv = require('iconv-lite');
|
|
10
10
|
const path = require('path');
|
|
11
11
|
const crypto = require('crypto');
|
|
12
|
+
const imageTransform = require('@tryghost/image-transform');
|
|
12
13
|
|
|
13
14
|
// Some sites block non-standard user agents so we need to mimic a typical browser
|
|
14
15
|
// Note: the Ghost/5.0 string _may_ be in use by 3rd parties so use caution when updating across majors
|
|
@@ -39,7 +40,26 @@ const messages = {
|
|
|
39
40
|
insufficientMetadata: 'URL contains insufficient metadata.',
|
|
40
41
|
unknownProvider: 'No provider found for supplied URL.',
|
|
41
42
|
unableToFetchOembed: 'Unable to fetch requested embed.',
|
|
42
|
-
unauthorized: 'URL contains a private resource.'
|
|
43
|
+
unauthorized: 'URL contains a private resource.',
|
|
44
|
+
unconvertibleSvg: 'SVG image is too large or compressed to convert.'
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const SVG_RASTER_SIZE = 256;
|
|
48
|
+
const SVG_RASTER_TIMEOUT_SECONDS = 10;
|
|
49
|
+
const MAX_SVG_BYTES = 32 * 1024;
|
|
50
|
+
const GZIP_MAGIC = [0x1f, 0x8b];
|
|
51
|
+
|
|
52
|
+
const SVG_EXTENSIONS = new Set(['.svg', '.svgz']);
|
|
53
|
+
const SVG_SNIFF_BYTES = 1024;
|
|
54
|
+
|
|
55
|
+
const shouldRasterize = (buffer, ext) => {
|
|
56
|
+
if (SVG_EXTENSIONS.has(ext.toLowerCase())) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const head = buffer.subarray(0, SVG_SNIFF_BYTES).toString('utf8').trimStart();
|
|
61
|
+
|
|
62
|
+
return head.startsWith('<') && /<svg[\s:>]/i.test(head);
|
|
43
63
|
};
|
|
44
64
|
|
|
45
65
|
/**
|
|
@@ -175,14 +195,35 @@ class OEmbedService {
|
|
|
175
195
|
}
|
|
176
196
|
|
|
177
197
|
// Fetch image buffer from the URL
|
|
178
|
-
|
|
198
|
+
let imageBuffer = await this.fetchImageBuffer(imageUrl);
|
|
179
199
|
|
|
180
200
|
// Extract file name from URL
|
|
181
201
|
const fileName = path.basename(new URL(imageUrl).pathname);
|
|
182
|
-
|
|
202
|
+
let ext = path.extname(fileName);
|
|
183
203
|
const baseName = ext ? path.basename(fileName, ext) : fileName;
|
|
184
204
|
const name = this.imageStore.getSanitizedFileName(baseName);
|
|
185
205
|
|
|
206
|
+
if (shouldRasterize(imageBuffer, ext)) {
|
|
207
|
+
if (imageBuffer.length > MAX_SVG_BYTES || GZIP_MAGIC.every((byte, index) => imageBuffer[index] === byte)) {
|
|
208
|
+
throw new errors.ValidationError({
|
|
209
|
+
message: tpl(messages.unconvertibleSvg),
|
|
210
|
+
context: imageUrl
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
imageBuffer = await imageTransform.resizeFromBuffer(imageBuffer, {
|
|
215
|
+
// without `format` this returns the original bytes whenever they
|
|
216
|
+
// are smaller, storing the SVG under a .png name
|
|
217
|
+
format: 'png',
|
|
218
|
+
width: SVG_RASTER_SIZE,
|
|
219
|
+
height: SVG_RASTER_SIZE,
|
|
220
|
+
withoutEnlargement: false,
|
|
221
|
+
timeout: SVG_RASTER_TIMEOUT_SECONDS
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
ext = '.png';
|
|
225
|
+
}
|
|
226
|
+
|
|
186
227
|
const uniqueFileName = `${name}-${crypto.randomUUID()}${ext}`;
|
|
187
228
|
const targetPath = path.join(imageType, uniqueFileName);
|
|
188
229
|
|
|
@@ -312,6 +312,17 @@ class PostsService {
|
|
|
312
312
|
});
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
+
// The `comments.in_reply_to_id` references form chains between a post's
|
|
316
|
+
// comments, which MySQL cannot resolve while cascade-deleting them
|
|
317
|
+
// alongside `comments.parent_id`. Clear the references first so the
|
|
318
|
+
// `comments.post_id` cascade delete can do its job
|
|
319
|
+
await this.models.Post.bulkEdit(deleteIds, 'comments', {
|
|
320
|
+
data: {in_reply_to_id: null},
|
|
321
|
+
column: 'post_id',
|
|
322
|
+
transacting: options.transacting,
|
|
323
|
+
throwErrors: true
|
|
324
|
+
});
|
|
325
|
+
|
|
315
326
|
// Posts and emails
|
|
316
327
|
await this.models.Post.bulkDestroy(deleteEmailIds, 'emails', {transacting: options.transacting, throwErrors: true});
|
|
317
328
|
const result = await this.models.Post.bulkDestroy(deleteIds, 'posts', {...options, throwErrors: true});
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
const crypto = require('crypto');
|
|
2
|
-
const fs = require('fs-extra');
|
|
3
1
|
const debug = require('@tryghost/debug')('services:route-settings:service');
|
|
4
2
|
const logging = require('@tryghost/logging');
|
|
5
3
|
const errors = require('@tryghost/errors');
|
|
@@ -9,12 +7,6 @@ const messages = {
|
|
|
9
7
|
loadError: 'Could not load routes.yaml file.'
|
|
10
8
|
};
|
|
11
9
|
|
|
12
|
-
/**
|
|
13
|
-
* md5 of the expanded default route settings — the routes_hash value for a
|
|
14
|
-
* site that has never customised its routes.
|
|
15
|
-
*/
|
|
16
|
-
const DEFAULT_ROUTES_SETTING_HASH = '3d180d52c663d173a6be791ef411ed01';
|
|
17
|
-
|
|
18
10
|
function isStoredContentError(err) {
|
|
19
11
|
return err.errorType === 'ValidationError' || err.errorType === 'IncorrectUsageError';
|
|
20
12
|
}
|
|
@@ -27,26 +19,21 @@ class DynamicRoutingService {
|
|
|
27
19
|
constructor() {
|
|
28
20
|
/** @type {RouteSettingsStore} */
|
|
29
21
|
this.store = null;
|
|
30
|
-
this.settingsLoader = null;
|
|
31
22
|
this.routerManager = null;
|
|
32
23
|
this.urlService = null;
|
|
33
24
|
}
|
|
34
25
|
|
|
35
26
|
/**
|
|
36
|
-
* Wire the storage-layer
|
|
37
|
-
*
|
|
38
|
-
*
|
|
27
|
+
* Wire the storage-layer dependency so the API surface (upload, download)
|
|
28
|
+
* works immediately after boot — even when the frontend is disabled and
|
|
29
|
+
* `start()` is never called.
|
|
39
30
|
*
|
|
40
31
|
* @param {object} deps
|
|
41
32
|
* @param {RouteSettingsStore} deps.store - adapter-manager provided store
|
|
42
|
-
* @param {object} [deps.settingsLoader] - legacy SettingsLoader used as a
|
|
43
|
-
* read-path fallback when the stricter store parser rejects a file
|
|
44
|
-
* (see loadRouteSettings)
|
|
45
33
|
*/
|
|
46
|
-
configure({store
|
|
34
|
+
configure({store}) {
|
|
47
35
|
debug('configure');
|
|
48
36
|
this.store = store;
|
|
49
|
-
this.settingsLoader = settingsLoader;
|
|
50
37
|
}
|
|
51
38
|
|
|
52
39
|
/**
|
|
@@ -67,49 +54,31 @@ class DynamicRoutingService {
|
|
|
67
54
|
}
|
|
68
55
|
|
|
69
56
|
async loadRouteSettings() {
|
|
70
|
-
const {expandRouteSettings} = require('./activation-bridge');
|
|
71
|
-
|
|
72
57
|
try {
|
|
73
|
-
return
|
|
58
|
+
return await this.store.get();
|
|
74
59
|
} catch (err) {
|
|
75
|
-
|
|
76
|
-
|
|
60
|
+
// A stored-content error means the site's routes.yaml is invalid —
|
|
61
|
+
// either it fails validation or it isn't parseable YAML. Log a
|
|
62
|
+
// targeted error so the failure is easy to spot in the logs, then
|
|
63
|
+
// rethrow so the caller surfaces the genuine error rather than
|
|
64
|
+
// silently degrading.
|
|
65
|
+
if (isStoredContentError(err)) {
|
|
66
|
+
logging.error(new errors.InternalServerError({
|
|
67
|
+
message: 'Route settings could not be loaded because the routes.yaml file is invalid. Please fix the file.',
|
|
68
|
+
code: 'ROUTE_SETTINGS_VALIDATION_ERROR',
|
|
69
|
+
err,
|
|
70
|
+
errorDetails: {reason: err.message}
|
|
71
|
+
}));
|
|
77
72
|
}
|
|
78
|
-
logging.error(new errors.InternalServerError({
|
|
79
|
-
message: 'Route settings failed the stricter validation and were loaded with the legacy validator instead. Please review the routes.yaml file.',
|
|
80
|
-
code: 'ROUTE_SETTINGS_VALIDATION_FALLBACK',
|
|
81
|
-
err,
|
|
82
|
-
errorDetails: {reason: err.message}
|
|
83
|
-
}));
|
|
84
|
-
|
|
85
|
-
return this.settingsLoader.loadSettings();
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
getDefaultHash() {
|
|
90
|
-
return DEFAULT_ROUTES_SETTING_HASH;
|
|
91
|
-
}
|
|
92
73
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return crypto.createHash('md5')
|
|
97
|
-
.update(JSON.stringify(expanded), 'binary')
|
|
98
|
-
.digest('hex');
|
|
74
|
+
throw err;
|
|
75
|
+
}
|
|
99
76
|
}
|
|
100
77
|
|
|
101
78
|
async download() {
|
|
102
|
-
|
|
103
|
-
const settings = await this.store.get();
|
|
104
|
-
|
|
105
|
-
return settings.yamlSource;
|
|
106
|
-
} catch (err) {
|
|
107
|
-
if (!this.settingsLoader || !isStoredContentError(err)) {
|
|
108
|
-
throw err;
|
|
109
|
-
}
|
|
79
|
+
const settings = await this.store.get();
|
|
110
80
|
|
|
111
|
-
|
|
112
|
-
}
|
|
81
|
+
return settings.yamlSource;
|
|
113
82
|
}
|
|
114
83
|
|
|
115
84
|
async upload(yamlContent) {
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
const config = require('../../../shared/config');
|
|
2
|
-
const parseYaml = require('./yaml-parser');
|
|
3
1
|
const DynamicRoutingService = require('./dynamic-routing-service');
|
|
4
2
|
|
|
5
3
|
const service = new DynamicRoutingService();
|
|
@@ -7,15 +5,9 @@ const service = new DynamicRoutingService();
|
|
|
7
5
|
module.exports = {
|
|
8
6
|
init: async () => {
|
|
9
7
|
const adapterManager = require('../adapter-manager').default;
|
|
10
|
-
const SettingsLoader = require('./settings-loader');
|
|
11
|
-
const SettingsPathManager = require('./settings-path-manager');
|
|
12
|
-
|
|
13
|
-
const settingsPathManager = new SettingsPathManager({type: 'routes', paths: [config.getContentPath('settings')]});
|
|
14
|
-
const settingsLoader = new SettingsLoader({parseYaml, settingFilePath: settingsPathManager.getDefaultFilePath()});
|
|
15
8
|
|
|
16
9
|
service.configure({
|
|
17
|
-
store: adapterManager.getAdapter('route-settings')
|
|
18
|
-
settingsLoader
|
|
10
|
+
store: adapterManager.getAdapter('route-settings')
|
|
19
11
|
});
|
|
20
12
|
},
|
|
21
13
|
|
|
@@ -27,10 +19,6 @@ module.exports = {
|
|
|
27
19
|
return service.loadRouteSettings.bind(service);
|
|
28
20
|
},
|
|
29
21
|
|
|
30
|
-
get getDefaultHash() {
|
|
31
|
-
return service.getDefaultHash.bind(service);
|
|
32
|
-
},
|
|
33
|
-
|
|
34
22
|
/**
|
|
35
23
|
* Methods backing the Admin API settings endpoint — delegate to the
|
|
36
24
|
* service instance so the endpoint stays decoupled from service wiring.
|
|
@@ -41,9 +29,6 @@ module.exports = {
|
|
|
41
29
|
},
|
|
42
30
|
get download() {
|
|
43
31
|
return service.download.bind(service);
|
|
44
|
-
},
|
|
45
|
-
get getCurrentHash() {
|
|
46
|
-
return service.getCurrentHash.bind(service);
|
|
47
32
|
}
|
|
48
33
|
}
|
|
49
34
|
};
|