ghost 6.55.0 → 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-custom-field-types-0.0.0.tgz +0 -0
- package/components/tryghost-kg-default-cards-10.3.3.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/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-sjHLx5dy.js → account-migration-D3O_yVck.js} +1 -1
- package/core/built/admin/assets/activitypub/{account-migration-DoYXtnZ8.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-Bxeh6MKW.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-D8sFfdpC.js → dist-BEfX-0aW.js} +12 -12
- package/core/built/admin/assets/activitypub/{domain-CWueFW6q.js → domain-AaDlZlrA.js} +9 -9
- package/core/built/admin/assets/activitypub/{edit-profile-BGxPU7N5.js → edit-profile-DPi8Tl5M.js} +945 -945
- package/core/built/admin/assets/activitypub/{explore-DBK-u0Ch.js → explore-CxTzjILQ.js} +8 -8
- package/core/built/admin/assets/activitypub/{feed-Bcir5RQZ.js → feed-DPv5x2MX.js} +33 -33
- package/core/built/admin/assets/activitypub/{field-BV-CIT3d.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-yC0nfvzl.js → inbox-DSrF_i1O.js} +85 -85
- package/core/built/admin/assets/activitypub/{label-DTRS9TDk.js → label-CPjFBbo7.js} +3 -3
- package/core/built/admin/assets/activitypub/layout-CTgrezN7.js +1 -0
- package/core/built/admin/assets/activitypub/{moderation-Dwl9t0pb.js → moderation-BkDC1XVZ.js} +11 -11
- package/core/built/admin/assets/activitypub/{note-B-zKsZ7r.js → note-xEyH4xT-.js} +7 -7
- package/core/built/admin/assets/activitypub/{notifications-DjJZD54t.js → notifications-DClaL3ui.js} +9 -9
- package/core/built/admin/assets/activitypub/{preferences-CoU8vs6d.js → preferences-DpnOa-4Z.js} +20 -20
- package/core/built/admin/assets/activitypub/{profile-BAsaJekx.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-CLH3OVTP.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-DA6pNJ-Q.js → settings-CV2fFu6D.js} +13 -13
- package/core/built/admin/assets/activitypub/{site-CQD42Pw5.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-Cg0saow1.js → tabs-BUbz7tea.js} +317 -315
- 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-Br4aOWOh.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-DjEWNhp3.js → audience-CkBb2LlO.js} +1 -1
- package/core/built/admin/assets/{automated-emails-DRy7qY-J.js → automated-emails-6t2bIRWf.js} +1 -1
- package/core/built/admin/assets/{automations-Bx3VcFIL.js → automations-cMllU545.js} +1 -1
- package/core/built/admin/assets/{bluesky-sharing-ClMd0-Ft.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-BD0U2ygg.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-fjQKRVyY.js → chart-helpers-DV0FkdcD.js} +1 -1
- package/core/built/admin/assets/{checkbox-CWa28lgO.js → checkbox-BIl077a0.js} +1 -1
- package/core/built/admin/assets/{chunk.481.6c7395a49484bb4583aa.js → chunk.332.05991a101d67f732dec3.js} +43 -42
- 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-CCr0rBWS.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-BFp58EMk.js → date-range-select-dXc-GsVe.js} +1 -1
- package/core/built/admin/assets/{disabled-sources-indicator-CUxMfiPa.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-B5ogXUBa.js +1 -0
- package/core/built/admin/assets/{dropzone-DlK-K2DQ.js → dropzone-BnFG7dDP.js} +1 -1
- package/core/built/admin/assets/{edit-profile-BKT-REJw.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-ClUpJZOp.js → explore-Ca1y8HW_.js} +1 -1
- package/core/built/admin/assets/{feed-DF92dUMP.js → feed-x4iFMBP4.js} +1 -1
- package/core/built/admin/assets/{field-Do7lieBA.js → field-ChCAabZg.js} +1 -1
- package/core/built/admin/assets/filter-sources-LdtH1e-T.js +1 -0
- package/core/built/admin/assets/filters-CLehPo_D.js +1 -0
- package/core/built/admin/assets/filters-CjVKRP8U.js +1 -0
- package/core/built/admin/assets/{ghost-c4057474943effc568c5659ccc4909d7.js → ghost-7f01131115186d1af84ead76e79646dd.js} +149 -129
- package/core/built/admin/assets/{ghost-a9c788f6f0b1ea6cf0f14ca4b13f4496.css → ghost-d38c30b866e59f2e06a357b5eb844fe5.css} +1 -1
- package/core/built/admin/assets/{ghost-dark-db36d795d21c61426837df40af270966.css → ghost-dark-1bffd5099ba7a749303133efacf8480b.css} +1 -1
- package/core/built/admin/assets/{gift-link-modal-f9VIMNDv.js → gift-link-modal-Buqh2vXh.js} +1 -1
- package/core/built/admin/assets/{growth-B3I6gNLm.js → growth-DajOBdN0.js} +1 -1
- package/core/built/admin/assets/{growth-CVw1My4S.js → growth-Dy2UOSay.js} +2 -2
- package/core/built/admin/assets/{inbox-CFBSu4Ed.js → inbox-BUOLDoji.js} +12 -12
- 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 +9535 -9527
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +124 -124
- package/core/built/admin/assets/{koenig-lexical-f8vHRYvz.js → koenig-lexical-BFc4yFed.js} +86 -86
- package/core/built/admin/assets/{kpi-card-DSap9rAy.js → kpi-card-BN-E6WYX.js} +1 -1
- package/core/built/admin/assets/{kpi-card-DU0rn9Cj.js → kpi-card-CCt-qeWD.js} +1 -1
- package/core/built/admin/assets/{kpi-helpers-Dz33AV6h.js → kpi-helpers-DlxTbTs4.js} +1 -1
- package/core/built/admin/assets/{kpi-tabs-BtTTrKvJ.js → kpi-tabs-BlcUttpn.js} +1 -1
- package/core/built/admin/assets/{label-DS8Mi6G5.js → label-AVrxwmsz.js} +1 -1
- package/core/built/admin/assets/{label-picker-1DvSuLA1.js → label-picker-DVnpu_IF.js} +1 -1
- 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-CQpVFJu4.js → members-oLPiSkXD.js} +2 -2
- package/core/built/admin/assets/modals-CB7v-k3T.js +31 -0
- package/core/built/admin/assets/{moderation-DySbgyUS.js → moderation-CQ3urTFE.js} +1 -1
- package/core/built/admin/assets/newsletter-rbeDX9Wk.js +1 -0
- package/core/built/admin/assets/newsletters-vt3Ni1Ov.js +1 -0
- package/core/built/admin/assets/{note-C8kxrz6w.js → note-Dk877BTv.js} +1 -1
- package/core/built/admin/assets/{notifications-CgtSfX7t.js → notifications-CPlLjPxO.js} +1 -1
- package/core/built/admin/assets/{onboarding-route-4qtziA9d.js → onboarding-route-DdStOtjy.js} +1 -1
- package/core/built/admin/assets/{overview-Bf2sbznq.js → overview--4V-kx9c.js} +1 -1
- package/core/built/admin/assets/overview-DSLv30oz.js +1 -0
- package/core/built/admin/assets/{page-header-B3Wb9Jqv.js → page-header-pp4dWmOS.js} +1 -1
- package/core/built/admin/assets/{post-analytics-We-ko5Cb.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-B6UynTGb.js → post-analytics-provider-Btml5vJI.js} +1 -1
- package/core/built/admin/assets/{post-share-modal-DFfO93_0.js → post-share-modal-AcUZkbfL.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/{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-WXNeWyU1.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-DsgW3wbg.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-Bb_FCl7e.js → sort-button-Df4t2AL8.js} +1 -1
- package/core/built/admin/assets/{source-icon-BeaRxUn9.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-Bjj_kmJ1.js → stats-C_lfxZAw.js} +1 -1
- package/core/built/admin/assets/{stats-view-BxPbW6K1.js → stats-view-Cxot1iD8.js} +1 -1
- package/core/built/admin/assets/{step-1-B1e3-1F8.js → step-1-ogAA4lDr.js} +1 -1
- package/core/built/admin/assets/{step-2-C5WaeBJo.js → step-2-C5Xini-7.js} +1 -1
- package/core/built/admin/assets/{step-3-B46iVPwu.js → step-3-CZv1FV81.js} +2 -2
- package/core/built/admin/assets/{switch-z5Y6Uzt6.js → switch-CyQcwMAv.js} +1 -1
- package/core/built/admin/assets/{table-CB-4_HBJ.js → table-DFj1wiYv.js} +1 -1
- package/core/built/admin/assets/{tabs-CCEgKYzq.js → tabs-D4y1gqUd.js} +1 -1
- package/core/built/admin/assets/{tags-Cg2VJGmG.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/{toggle-group-b3IlxvhV.js → toggle-group-R2QMFvXW.js} +1 -1
- package/core/built/admin/assets/{topic-filter-DAFT87tQ.js → topic-filter-DGDxDalo.js} +1 -1
- package/core/built/admin/assets/{use-analytics-data-S0FTd43s.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-CIaiHOVb.js → use-growth-stats-aNXJOmQr.js} +1 -1
- package/core/built/admin/assets/{use-post-referrers-Cq2vBPrA.js → use-post-referrers-CG0SZioj.js} +1 -1
- package/core/built/admin/assets/{use-reply-chain-data-DBDRtGu-.js → use-reply-chain-data-RLdPKRmS.js} +1 -1
- package/core/built/admin/assets/{use-simple-pagination-CjYNxdwe.js → use-simple-pagination-DgcF69KI.js} +1 -1
- package/core/built/admin/assets/{use-tinybird-query-falHAtuK.js → use-tinybird-query-C1R_Kbmy.js} +1 -1
- package/core/built/admin/assets/{use-tinybird-token-D7-8ND1y.js → use-tinybird-token-C9fPjVM_.js} +1 -1
- package/core/built/admin/assets/{virtual-list-CvnC9zF2.js → virtual-list-BmTUzeoF.js} +1 -1
- package/core/built/admin/assets/{web-B_5GZRvH.js → web-BqW0b0_C.js} +1 -1
- package/core/built/admin/assets/web-Cy5GKMWM.js +2 -0
- package/core/built/admin/index.html +16 -16
- package/core/frontend/services/data/fetch-data.js +10 -5
- 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 +9 -13
- package/core/frontend/services/routing/static-routes-router.js +4 -4
- package/core/frontend/services/routing/taxonomy-router.js +6 -2
- 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/fixtures/fixtures.json +2 -2
- package/core/server/data/schema/schema.js +1 -0
- package/core/server/models/post.js +10 -1
- package/core/server/services/automations/automations-repository.ts +3 -1
- package/core/server/services/automations/database-automations-repository.js +33 -14
- package/core/server/services/automations/database-automations-repository.ts +41 -15
- 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 +3 -1
- package/core/server/services/automations/poll.ts +4 -1
- package/core/server/services/automations/service.js +5 -1
- package/core/server/services/automations/service.ts +5 -1
- 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/link-tracking/link-click-repository.js +6 -1
- package/core/server/services/link-tracking/link-click-tracking-service.js +12 -3
- package/core/server/services/member-welcome-emails/member-welcome-email-renderer.js +4 -3
- package/core/server/services/member-welcome-emails/service.js +7 -3
- package/core/server/services/members/api.js +1 -2
- package/core/server/services/members/import-export/import/importer.ts +3 -2
- package/core/server/services/members/import-export/index.js +10 -2
- package/core/server/services/members/import-export/index.ts +14 -4
- 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-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 +118 -2
- package/core/server/services/members-custom-fields/values-service.ts +134 -4
- 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 +1 -3
- 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/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/web/gift-preview/controller.js +10 -25
- package/core/shared/labs.js +2 -2
- package/package.json +10 -9
- package/pnpm-lock.yaml +28 -18
- package/pnpm-workspace.yaml +5 -4
- package/components/tryghost-kg-default-nodes-2.1.3.tgz +0 -0
- package/core/built/admin/assets/activitypub/layout-qx-bUL1k.js +0 -1
- package/core/built/admin/assets/chunk.524.cffd399783a519e404a9.js +0 -25
- package/core/built/admin/assets/chunk.582.eebf0d0b2d202bb746a5.js +0 -27
- package/core/built/admin/assets/comments-DyUSMUdj.js +0 -1
- package/core/built/admin/assets/dist-CjY-f5Z1.js +0 -1
- package/core/built/admin/assets/domain-C0bV2UV8.js +0 -1
- package/core/built/admin/assets/editor-Dp865O-4.js +0 -8
- package/core/built/admin/assets/filter-sources-BV2rnpfA.js +0 -1
- package/core/built/admin/assets/filters-CUXJ2Nm9.js +0 -1
- package/core/built/admin/assets/filters-DfbIWc_l.js +0 -1
- package/core/built/admin/assets/index-9Cw8z3rA.css +0 -1
- package/core/built/admin/assets/index-BGG7NckO.js +0 -67
- package/core/built/admin/assets/input-group-BSgG_lVw.js +0 -1
- package/core/built/admin/assets/labels-CDSUt4J_.js +0 -1
- package/core/built/admin/assets/layout-yQEi2qDM.js +0 -1
- package/core/built/admin/assets/link-helpers-7EYqvvHs.js +0 -1
- package/core/built/admin/assets/member-custom-fields-yOMHyAg9.js +0 -1
- package/core/built/admin/assets/member-detail-BAq7gDaa.js +0 -1
- package/core/built/admin/assets/modals-ReORvWnu.js +0 -31
- package/core/built/admin/assets/newsletter-CJQvtHqN.js +0 -1
- package/core/built/admin/assets/newsletters-B3QQXfGT.js +0 -1
- package/core/built/admin/assets/nql-BsgFMd93.js +0 -1
- package/core/built/admin/assets/overview-CBfePxcr.js +0 -1
- package/core/built/admin/assets/post-analytics-header-Bmc9su6q.js +0 -1
- package/core/built/admin/assets/preferences-DJPfHEGu.js +0 -5
- package/core/built/admin/assets/profile-DqBQp-gT.js +0 -1
- package/core/built/admin/assets/select-C5_xEt5b.js +0 -1
- package/core/built/admin/assets/settings-BwiVIXau.js +0 -4
- package/core/built/admin/assets/social-urls-BQPFEk1_.js +0 -8
- package/core/built/admin/assets/textarea-Bjccvh_E.js +0 -1
- package/core/built/admin/assets/use-filter-params-Gpoq2E1L.js +0 -1
- package/core/built/admin/assets/web-jFYWCKDn.js +0 -2
- 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 -174
- package/core/server/services/route-settings/activation-bridge.ts +0 -250
|
@@ -14,7 +14,6 @@ class PaymentsService {
|
|
|
14
14
|
* @param {import('../../../offers/application/offers-api')} deps.offersAPI
|
|
15
15
|
* @param {import('../../../stripe/stripe-api')} deps.stripeAPIService
|
|
16
16
|
* @param {{get(key: string): any}} deps.settingsCache
|
|
17
|
-
* @param {{service: import('../../../gifts/gift-service').GiftService}} deps.giftService
|
|
18
17
|
*/
|
|
19
18
|
constructor(deps) {
|
|
20
19
|
/** @private */
|
|
@@ -31,9 +30,6 @@ class PaymentsService {
|
|
|
31
30
|
this.stripeAPIService = deps.stripeAPIService;
|
|
32
31
|
/** @private */
|
|
33
32
|
this.settingsCache = deps.settingsCache;
|
|
34
|
-
/** @private */
|
|
35
|
-
this.giftService = deps.giftService;
|
|
36
|
-
|
|
37
33
|
DomainEvents.subscribe(OfferCreatedEvent, async (event) => {
|
|
38
34
|
await this.getCouponForOffer(event.data.offer.id);
|
|
39
35
|
});
|
|
@@ -65,7 +61,7 @@ class PaymentsService {
|
|
|
65
61
|
* @param {Tier.Cadence} params.cadence
|
|
66
62
|
* @param {Offer} [params.offer]
|
|
67
63
|
* @param {Member} [params.member]
|
|
68
|
-
* @param {
|
|
64
|
+
* @param {number|null} [params.giftTrialDays]
|
|
69
65
|
* @param {Object.<string, any>} [params.metadata]
|
|
70
66
|
* @param {string} params.successUrl
|
|
71
67
|
* @param {string} params.cancelUrl
|
|
@@ -73,7 +69,7 @@ class PaymentsService {
|
|
|
73
69
|
*
|
|
74
70
|
* @returns {Promise<URL>}
|
|
75
71
|
*/
|
|
76
|
-
async getPaymentLink({tier, cadence, offer, member,
|
|
72
|
+
async getPaymentLink({tier, cadence, offer, member, giftTrialDays, metadata, successUrl, cancelUrl, email}) {
|
|
77
73
|
let coupon = null;
|
|
78
74
|
let trialDays = null;
|
|
79
75
|
if (offer) {
|
|
@@ -94,12 +90,10 @@ class PaymentsService {
|
|
|
94
90
|
}
|
|
95
91
|
}
|
|
96
92
|
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
trialDays = Math.min(remainingDays, 730); // Stripe max trial period
|
|
102
|
-
}
|
|
93
|
+
// Gift subscriptions owns the continuation calculation. This adapter only
|
|
94
|
+
// translates the stable decision into the recurring Stripe checkout.
|
|
95
|
+
if (trialDays === null && giftTrialDays !== undefined) {
|
|
96
|
+
trialDays = giftTrialDays;
|
|
103
97
|
}
|
|
104
98
|
|
|
105
99
|
let customer = null;
|
|
@@ -163,62 +157,6 @@ class PaymentsService {
|
|
|
163
157
|
return session.url;
|
|
164
158
|
}
|
|
165
159
|
|
|
166
|
-
/**
|
|
167
|
-
* @param {object} params
|
|
168
|
-
* @param {import('../../../tiers/tier')} params.tier
|
|
169
|
-
* @param {'month'|'year'} params.cadence
|
|
170
|
-
* @param {number} params.duration
|
|
171
|
-
* @param {string} params.successUrl
|
|
172
|
-
* @param {string} params.cancelUrl
|
|
173
|
-
* @param {object} params.metadata
|
|
174
|
-
* @param {object} [params.member]
|
|
175
|
-
* @param {boolean} params.isAuthenticated
|
|
176
|
-
* @param {string} [params.email]
|
|
177
|
-
*
|
|
178
|
-
* @returns {Promise<string>}
|
|
179
|
-
*/
|
|
180
|
-
async getGiftPaymentLink({tier, cadence, duration, metadata, successUrl, cancelUrl, member, isAuthenticated, email}) {
|
|
181
|
-
let customer = null;
|
|
182
|
-
if (member && isAuthenticated) {
|
|
183
|
-
customer = await this.getCustomerForMember(member);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const amount = tier.getPrice(cadence);
|
|
187
|
-
const currency = tier.currency.toLowerCase();
|
|
188
|
-
|
|
189
|
-
const token = this.giftService.service.generateToken();
|
|
190
|
-
|
|
191
|
-
const successUrlObj = new URL(successUrl);
|
|
192
|
-
successUrlObj.searchParams.set('stripe', 'gift-purchase-success');
|
|
193
|
-
successUrlObj.searchParams.set('gift_token', token);
|
|
194
|
-
successUrlObj.searchParams.set('gift_tier', tier.id.toHexString());
|
|
195
|
-
successUrlObj.searchParams.set('gift_cadence', cadence);
|
|
196
|
-
|
|
197
|
-
const data = {
|
|
198
|
-
amount,
|
|
199
|
-
currency,
|
|
200
|
-
tierName: tier.name,
|
|
201
|
-
cadence,
|
|
202
|
-
duration,
|
|
203
|
-
metadata: {
|
|
204
|
-
...metadata,
|
|
205
|
-
ghost_gift: 'true',
|
|
206
|
-
gift_token: token,
|
|
207
|
-
tier_id: tier.id.toHexString(),
|
|
208
|
-
cadence,
|
|
209
|
-
duration: String(duration)
|
|
210
|
-
},
|
|
211
|
-
successUrl: successUrlObj.toString(),
|
|
212
|
-
cancelUrl,
|
|
213
|
-
customer,
|
|
214
|
-
customerEmail: !customer && email ? email : null
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
const session = await this.stripeAPIService.createGiftCheckoutSession(data);
|
|
218
|
-
|
|
219
|
-
return session.url;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
160
|
async getCustomerForMember(member) {
|
|
223
161
|
const rows = await this.StripeCustomerModel.where({
|
|
224
162
|
member_id: member.id
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DbCustomFieldValueWithField = exports.DbCustomFieldValue = exports.DbCustomField = exports.FieldStatusSchema = exports.FIELD_STATUS = void 0;
|
|
3
|
+
exports.DbCustomFieldValueWithField = exports.StoredCompositeValue = exports.DbCustomFieldValue = exports.DbCustomField = exports.FieldStatusSchema = exports.FIELD_STATUS = void 0;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
5
|
const custom_field_types_1 = require("@tryghost/custom-field-types");
|
|
6
6
|
const db_date_1 = require("../../lib/db-date");
|
|
@@ -37,6 +37,13 @@ exports.DbCustomFieldValue = zod_1.z.object({
|
|
|
37
37
|
created_at: db_date_1.DbDate,
|
|
38
38
|
updated_at: db_date_1.DbDate.nullable()
|
|
39
39
|
});
|
|
40
|
+
// A composite value once decoded: sub-field keys to values, and no narrower. Which
|
|
41
|
+
// sub-fields a composite has is its field type's business, not the row's.
|
|
42
|
+
//
|
|
43
|
+
// It doubles as the guard on a stored blob. A JSON column can just as easily hold an
|
|
44
|
+
// array, a null or a bare number, and a record rejects all three — so parsing against
|
|
45
|
+
// this is what narrows the type, with no assertion needed.
|
|
46
|
+
exports.StoredCompositeValue = zod_1.z.record(zod_1.z.string(), zod_1.z.unknown());
|
|
40
47
|
// The value join a read needs: the field's identity and type travel with the
|
|
41
48
|
// stored columns, so a row can be decoded without a second lookup. `type` is
|
|
42
49
|
// parsed as the field-type enum, which narrows it with no cast.
|
|
@@ -44,6 +44,14 @@ export const DbCustomFieldValue = z.object({
|
|
|
44
44
|
|
|
45
45
|
type CustomFieldValueRow = z.infer<typeof DbCustomFieldValue>;
|
|
46
46
|
|
|
47
|
+
// A composite value once decoded: sub-field keys to values, and no narrower. Which
|
|
48
|
+
// sub-fields a composite has is its field type's business, not the row's.
|
|
49
|
+
//
|
|
50
|
+
// It doubles as the guard on a stored blob. A JSON column can just as easily hold an
|
|
51
|
+
// array, a null or a bare number, and a record rejects all three — so parsing against
|
|
52
|
+
// this is what narrows the type, with no assertion needed.
|
|
53
|
+
export const StoredCompositeValue = z.record(z.string(), z.unknown());
|
|
54
|
+
|
|
47
55
|
// The value join a read needs: the field's identity and type travel with the
|
|
48
56
|
// stored columns, so a row can be decoded without a second lookup. `type` is
|
|
49
57
|
// parsed as the field-type enum, which narrows it with no cast.
|
|
@@ -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
|
|
@@ -188,28 +203,129 @@ class CustomFieldValuesService {
|
|
|
188
203
|
}
|
|
189
204
|
return writes;
|
|
190
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
|
+
}
|
|
191
279
|
/**
|
|
192
280
|
* Apply a plan from `planWrite`.
|
|
193
281
|
*
|
|
194
282
|
* Merge, not replace: only the fields in the plan are touched, so a caller
|
|
195
283
|
* that doesn't know about a field can't erase it.
|
|
196
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
|
+
*
|
|
197
300
|
* Always transactional, so a mid-batch failure rolls the batch back. Passed an
|
|
198
301
|
* executor it joins that transaction -- the importer passes its per-member one, so a
|
|
199
302
|
* failed value write takes the member with it; passed nothing it opens its own.
|
|
200
303
|
*/
|
|
201
|
-
async applyWrite(memberId, writes, executor = this.knex) {
|
|
304
|
+
async applyWrite(memberId, writes, executor = this.knex, { mergeComposites = false } = {}) {
|
|
202
305
|
if (writes.length === 0) {
|
|
203
306
|
return;
|
|
204
307
|
}
|
|
205
308
|
const apply = async (trx) => {
|
|
309
|
+
const stored = mergeComposites
|
|
310
|
+
? await this.storedComposites(trx, memberId, writes)
|
|
311
|
+
: new Map();
|
|
206
312
|
for (const { field, value } of writes) {
|
|
207
313
|
const target = { member_id: memberId, custom_field_id: field.id };
|
|
208
314
|
if (value === undefined) {
|
|
209
315
|
await trx(VALUES_TABLE).where(target).del();
|
|
210
316
|
continue;
|
|
211
317
|
}
|
|
212
|
-
|
|
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);
|
|
213
329
|
await trx(VALUES_TABLE)
|
|
214
330
|
.insert({ id: new bson_objectid_1.default().toHexString(), ...target, ...valueColumns, created_at: new Date() })
|
|
215
331
|
// The unique index on (member_id, custom_field_id) is
|
|
@@ -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
|
|
@@ -222,22 +237,126 @@ export class CustomFieldValuesService {
|
|
|
222
237
|
return writes;
|
|
223
238
|
}
|
|
224
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
|
+
|
|
225
320
|
/**
|
|
226
321
|
* Apply a plan from `planWrite`.
|
|
227
322
|
*
|
|
228
323
|
* Merge, not replace: only the fields in the plan are touched, so a caller
|
|
229
324
|
* that doesn't know about a field can't erase it.
|
|
230
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
|
+
*
|
|
231
341
|
* Always transactional, so a mid-batch failure rolls the batch back. Passed an
|
|
232
342
|
* executor it joins that transaction -- the importer passes its per-member one, so a
|
|
233
343
|
* failed value write takes the member with it; passed nothing it opens its own.
|
|
234
344
|
*/
|
|
235
|
-
async applyWrite(
|
|
345
|
+
async applyWrite(
|
|
346
|
+
memberId: string,
|
|
347
|
+
writes: PlannedWrite[],
|
|
348
|
+
executor: Knex = this.knex,
|
|
349
|
+
{mergeComposites = false}: {mergeComposites?: boolean} = {}
|
|
350
|
+
): Promise<void> {
|
|
236
351
|
if (writes.length === 0) {
|
|
237
352
|
return;
|
|
238
353
|
}
|
|
239
354
|
|
|
240
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
|
+
|
|
241
360
|
for (const {field, value} of writes) {
|
|
242
361
|
const target = {member_id: memberId, custom_field_id: field.id};
|
|
243
362
|
|
|
@@ -246,7 +365,18 @@ export class CustomFieldValuesService {
|
|
|
246
365
|
continue;
|
|
247
366
|
}
|
|
248
367
|
|
|
249
|
-
|
|
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);
|
|
250
380
|
await trx(VALUES_TABLE)
|
|
251
381
|
.insert({id: new ObjectID().toHexString(), ...target, ...valueColumns, created_at: new Date()})
|
|
252
382
|
// The unique index on (member_id, custom_field_id) is
|
|
@@ -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});
|
|
@@ -54,10 +54,8 @@ class DynamicRoutingService {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
async loadRouteSettings() {
|
|
57
|
-
const {buildRouterSettings} = require('./activation-bridge');
|
|
58
|
-
|
|
59
57
|
try {
|
|
60
|
-
return
|
|
58
|
+
return await this.store.get();
|
|
61
59
|
} catch (err) {
|
|
62
60
|
// A stored-content error means the site's routes.yaml is invalid —
|
|
63
61
|
// either it fails validation or it isn't parseable YAML. Log a
|