ghost 6.44.0 → 6.44.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/boot.js +9 -13
- package/core/bridge.js +6 -10
- package/core/built/admin/assets/{PolarAngleAxis-eFuSrR7S.js → PolarAngleAxis-DWP6qq-Z.js} +2 -2
- package/core/built/admin/assets/{_baseAssignValue-XVHfKew7.js → _baseAssignValue-N9f4o_XK.js} +1 -1
- package/core/built/admin/assets/{a-large-small-DOzPDzKp.js → a-large-small-BOAtcFiE.js} +1 -1
- package/core/built/admin/assets/{account-migration-Cx45M5qm.js → account-migration-CtZx_5ib.js} +1 -1
- package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +1 -1
- package/core/built/admin/assets/admin-x-settings/{code-editor-view-Bi8Orhyj.mjs → code-editor-view-BMovCjer.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-1_V9g9Pt.mjs → index-BT1n-g2b.mjs} +3 -3
- package/core/built/admin/assets/admin-x-settings/{index-e9saYQJa.mjs → index-BnowNBGd.mjs} +5 -5
- package/core/built/admin/assets/admin-x-settings/{index-CnvQi01U.mjs → index-CRpK6gF8.mjs} +3 -3
- package/core/built/admin/assets/admin-x-settings/{index-BmU6BOfy.mjs → index-CfXS3AZ8.mjs} +3 -3
- package/core/built/admin/assets/admin-x-settings/{index-EuMiBt6I.mjs → index-Cs8zML6u.mjs} +3 -3
- package/core/built/admin/assets/admin-x-settings/{index-B43MPc5u.mjs → index-DmpwO1w3.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-DOgG0XZa.mjs → index-DwFIdhcR.mjs} +3 -3
- package/core/built/admin/assets/admin-x-settings/{index-CWdxMIcZ.mjs → index-sNyPiIJA.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-Ojg_pBvq.mjs → index-xTILut54.mjs} +1259 -1259
- package/core/built/admin/assets/admin-x-settings/{modals-Ckbx9UQF.mjs → modals-Cm5t13os.mjs} +9 -9
- package/core/built/admin/assets/{arrow-right-DckEQtp9.js → arrow-right-BxdHN_En.js} +1 -1
- package/core/built/admin/assets/{at-sign-BChf68aS.js → at-sign-Cajf-dOf.js} +1 -1
- package/core/built/admin/assets/{audience-BYU5Bd7D.js → audience-CHNb9mCT.js} +1 -1
- package/core/built/admin/assets/{automated-email-design-Z26Qit0_.js → automated-email-design-B9C9mKUS.js} +2 -2
- package/core/built/admin/assets/{automated-emails-aBicreLv.js → automated-emails-DhLhkq8L.js} +1 -1
- package/core/built/admin/assets/{automations-vpH-d72N.js → automations-6-cgZovk.js} +1 -1
- package/core/built/admin/assets/{automations-Ddo22yPk.js → automations-CxJ-N_YK.js} +1 -1
- package/core/built/admin/assets/{avatar-flipboard-CoI3LQ-o.js → avatar-flipboard-f5nSMmek.js} +1 -1
- package/core/built/admin/assets/{bluesky-sharing-iVDkCMKN.js → bluesky-sharing-Dm2k8qZH.js} +1 -1
- package/core/built/admin/assets/{chart-CjM1VQqH.js → chart-BbJnjWey.js} +23 -23
- package/core/built/admin/assets/{checkbox-CC5kd_Kf.js → checkbox-CT7DNFAI.js} +1 -1
- package/core/built/admin/assets/{chevron-up-D__wzyXT.js → chevron-up-BxijmTT9.js} +1 -1
- package/core/built/admin/assets/{chunk.696.79505fa556d329d6e824.js → chunk.167.37bb0d7f6ac67fefa679.js} +3184 -3523
- package/core/built/admin/assets/{chunk.696.79505fa556d329d6e824.js.LICENSE.txt → chunk.167.37bb0d7f6ac67fefa679.js.LICENSE.txt} +1 -1
- package/core/built/admin/assets/{chunk.524.1ff649a6cc62dc38f538.js → chunk.524.ed812b62217210a913d4.js} +5 -5
- package/core/built/admin/assets/{chunk.582.408a40e0095e479e3d11.js → chunk.582.7db5c9f17bca5a484049.js} +6 -6
- package/core/built/admin/assets/{chunk.945.835812fcb16029abd7f9.js → chunk.834.f9db7c62b9457db93e8c.js} +3 -3
- package/core/built/admin/assets/{code-editor-view-C735d5EA.js → code-editor-view-D8slE0IA.js} +1 -1
- package/core/built/admin/assets/{comments-DgFPax3Y.js → comments-BvfR-fA8.js} +1 -1
- package/core/built/admin/assets/{content-helpers-DjugSfvm.js → content-helpers-DUWGLvgL.js} +1 -1
- package/core/built/admin/assets/{copy-D9JONGcK.js → copy-ht18t5gu.js} +1 -1
- package/core/built/admin/assets/{data-list-QWLEHG4y.js → data-list-Daha2dIs.js} +1 -1
- package/core/built/admin/assets/{deleted-feed-item-CwSCflTW.js → deleted-feed-item-ChP5bwgi.js} +1 -1
- package/core/built/admin/assets/{dropzone-lK_4vpWS.js → dropzone-h_qyk514.js} +1 -1
- package/core/built/admin/assets/{edit-profile-BmioJZHv.js → edit-profile-BT8RQpAG.js} +1 -1
- package/core/built/admin/assets/editor-s1mqd9_o.js +8 -0
- package/core/built/admin/assets/{empty-indicator-Dg64lT_g.js → empty-indicator-BIUvv6JE.js} +1 -1
- package/core/built/admin/assets/{en-DCwBNJ98.js → en-BEcDl42w.js} +1 -1
- package/core/built/admin/assets/{eye-psLxWnjO.js → eye-TPnIdamu.js} +1 -1
- package/core/built/admin/assets/{feed-TUcCSZHL.js → feed-BK3qvLWi.js} +1 -1
- package/core/built/admin/assets/{field-Uk8NOvtc.js → field-CSFsrvVy.js} +1 -1
- package/core/built/admin/assets/{filter-query-core-CbH_VKcG.js → filter-query-core-R8cQCO2w.js} +1 -1
- package/core/built/admin/assets/{filters-DmawZ5Uu.js → filters-Cp1voB1e.js} +1 -1
- package/core/built/admin/assets/gh-chart-BFsyOJD_.js +1 -0
- package/core/built/admin/assets/{ghost-2baa7ddc3630015f41c0efd13bd586b8.js → ghost-1881362e8a92dae1e3333a0484c173a3.js} +7 -6
- package/core/built/admin/assets/{growth-BAl-TyMy.js → growth-DIigOxF1.js} +1 -1
- package/core/built/admin/assets/{hash-Q1ou0OMZ.js → hash-B3iFVeCl.js} +1 -1
- package/core/built/admin/assets/{inbox-BAOtR6tN.js → inbox-2bPMLikt.js} +1 -1
- package/core/built/admin/assets/{index-BnJp51QE.js → index-52qUC8yv.js} +5 -5
- package/core/built/admin/assets/{index-BDIwushG.js → index-9j01Aj_X.js} +1 -1
- package/core/built/admin/assets/index-B9UVpmey.js +1 -0
- package/core/built/admin/assets/{index-DTeOW3Se.js → index-BXc5OF6x.js} +1 -1
- package/core/built/admin/assets/{index-Duc21aJ7.js → index-BaGL3IFe.js} +1 -1
- package/core/built/admin/assets/{index-DKC8uXHk.js → index-BdozZbt2.js} +2 -2
- package/core/built/admin/assets/{index-z951kI4K.js → index-Bgpz7M7i.js} +1 -1
- package/core/built/admin/assets/{index-BfSvX9is.js → index-Bp4O7TVf.js} +1 -1
- package/core/built/admin/assets/{index-C5S08C6P.js → index-BuqUmpe9.js} +1 -1
- package/core/built/admin/assets/{index-D7OtVnbo.js → index-CEF_Ofn0.js} +1 -1
- package/core/built/admin/assets/{index-Bs6rUYFS.js → index-CgSVv5Up.js} +58 -58
- package/core/built/admin/assets/{index-Ro9qXetv.js → index-Cr6x2czK.js} +1 -1
- package/core/built/admin/assets/{index-DJbgQjV6.js → index-CrbZj3Ju.js} +1 -1
- package/core/built/admin/assets/{index-BTBcWw_z.js → index-CrlAPDdD.js} +2 -2
- package/core/built/admin/assets/{index-B6SD2Qs-.js → index-CtrmMGWT.js} +1 -1
- package/core/built/admin/assets/{index-Bw0oyOFd.js → index-D-V_DAdt.js} +1 -1
- package/core/built/admin/assets/{index-DAJduTBl.js → index-D0poOSjw.js} +4 -4
- package/core/built/admin/assets/{index-CGT3W8fi.js → index-DWVGCPEC.js} +3 -3
- package/core/built/admin/assets/index-Dozbr_4s.css +1 -0
- package/core/built/admin/assets/{index-FyplpnYQ.js → index-S3Ai3lDy.js} +1 -1
- package/core/built/admin/assets/{index-DLr8D_Ks.js → index-VDhA5kgU.js} +1 -1
- package/core/built/admin/assets/{index-DRA1fORQ.js → index-fnIFqFMt.js} +1 -1
- package/core/built/admin/assets/{index-BhWQZObu.js → index-kA3UcdW7.js} +1 -1
- package/core/built/admin/assets/{input-group-DH32syTz.js → input-group-Fdl0S6pw.js} +1 -1
- package/core/built/admin/assets/{koenig-lexical-CxVcbgsH.js → koenig-lexical-REiYtzGD.js} +1 -1
- package/core/built/admin/assets/{kpi-card-DBWHDi3l.js → kpi-card-BS8vEj4c.js} +1 -1
- package/core/built/admin/assets/{kpi-card-BYhzWV5F.js → kpi-card-D3HEfOCP.js} +1 -1
- package/core/built/admin/assets/{kpi-tabs-Kxc-utyg.js → kpi-tabs-8vaI7yWq.js} +1 -1
- package/core/built/admin/assets/kpis-CkD0Xteg.js +1 -0
- package/core/built/admin/assets/label-DT38Gd2T.js +1 -0
- package/core/built/admin/assets/{links-Cg8O8ZGP.js → links-BK-azjCL.js} +1 -1
- package/core/built/admin/assets/{list-page-D2KCBsiy.js → list-page-DVpac3lc.js} +1 -1
- package/core/built/admin/assets/{loader-circle-nmLqhMZL.js → loader-circle-CIq5Cv-9.js} +1 -1
- package/core/built/admin/assets/{mail-D7cxYMd5.js → mail-gdeIU06V.js} +1 -1
- package/core/built/admin/assets/{main-layout-BXdUkeCo.js → main-layout-B2t-RiNE.js} +1 -1
- package/core/built/admin/assets/members-7z-tESWd.js +10 -0
- package/core/built/admin/assets/minus-BbGfVkll.js +1 -0
- package/core/built/admin/assets/{modals-VRvX8mK0.js → modals-DVIfijEz.js} +3 -3
- package/core/built/admin/assets/{moderation-DkT8qVJs.js → moderation-DwzDOhih.js} +1 -1
- package/core/built/admin/assets/{newsletter-sUT4ciRJ.js → newsletter-D1N8bUfp.js} +1 -1
- package/core/built/admin/assets/{newsletters-BNvAF2Oi.js → newsletters-7GSeQdQe.js} +1 -1
- package/core/built/admin/assets/{note-B0JhJv7s.js → note-Bn1-ZWDE.js} +1 -1
- package/core/built/admin/assets/offers-BsuLKYy9.js +1 -0
- package/core/built/admin/assets/{onboarding-route-IrO_SCb3.js → onboarding-route-C73r9kyO.js} +1 -1
- package/core/built/admin/assets/{overview-C2nk_B6i.js → overview-6U5p38zx.js} +1 -1
- package/core/built/admin/assets/{pagemenu-BGhty1iq.js → pagemenu-DrueoEX9.js} +1 -1
- package/core/built/admin/assets/{pencil-DiA3J4m5.js → pencil-Csnv8udS.js} +1 -1
- package/core/built/admin/assets/{pin-off-HWos3YHh.js → pin-off-BDeHGsSQ.js} +1 -1
- package/core/built/admin/assets/{post-analytics-context-BJwP-XAB.js → post-analytics-context-jKeUh2Ka.js} +1 -1
- package/core/built/admin/assets/{post-analytics-header-CZF5QmAD.js → post-analytics-header-C5QLn6Lm.js} +1 -1
- package/core/built/admin/assets/{post-analytics-DBUHbt1b.js → post-analytics-ngMqqbt9.js} +1 -1
- package/core/built/admin/assets/{post-share-modal-Bb8R05dH.js → post-share-modal-04B7JT4R.js} +1 -1
- package/core/built/admin/assets/posts/{alert-dialog-mmQvJUHQ.mjs → alert-dialog-CYvHtOQ5.mjs} +4 -4
- package/core/built/admin/assets/posts/{app-utils-CD0bhOLs.mjs → app-utils-Cs-8qje1.mjs} +3 -3
- package/core/built/admin/assets/posts/{automations-BL0WaXHA.mjs → automations-DE2rwx-Z.mjs} +25 -25
- package/core/built/admin/assets/posts/{automations-BmGKngyG.mjs → automations-Dq0_cprS.mjs} +4 -4
- package/core/built/admin/assets/posts/{avatar-CeJ0Zq_A.mjs → avatar-Dt_bGEgl.mjs} +7 -7
- package/core/built/admin/assets/posts/{button-CQCNdxY6.mjs → button-DO7JrPsL.mjs} +2 -2
- package/core/built/admin/assets/posts/{chevron-up-DaikhWC2.mjs → chevron-up-B8WyJjCr.mjs} +3 -3
- package/core/built/admin/assets/posts/{comments-DUSJ25vf.mjs → comments-B3-RpqJU.mjs} +1933 -1905
- package/core/built/admin/assets/posts/{data-list-BiiYXQes.mjs → data-list-BiZFP6Co.mjs} +5 -5
- package/core/built/admin/assets/posts/{dialog-DRN6olky.mjs → dialog-Bvl2PD8s.mjs} +3 -3
- package/core/built/admin/assets/posts/{editor-DN_Zz0m2.mjs → editor-BCtdAD74.mjs} +3230 -3130
- package/core/built/admin/assets/posts/{ellipsis-BIs4PL5-.mjs → ellipsis-B5H7Hudx.mjs} +2 -2
- package/core/built/admin/assets/posts/{empty-indicator-Wiukrx4T.mjs → empty-indicator-BRoAf6Jv.mjs} +2 -2
- package/core/built/admin/assets/posts/{filters-CJl5ebXd.mjs → filters-CffYQY_8.mjs} +12 -12
- package/core/built/admin/assets/posts/{get-site-timezone-JepxGwVr.mjs → get-site-timezone-CSrPVUfX.mjs} +2 -2
- package/core/built/admin/assets/posts/{growth-cNlqbz-x.mjs → growth--LVGRPtS.mjs} +13 -13
- package/core/built/admin/assets/posts/{heading-C_V6Ckxx.mjs → heading-BRQEP7IA.mjs} +2 -2
- package/core/built/admin/assets/posts/{heart-BMBWsLs2.mjs → heart-CC2f1qWb.mjs} +2 -2
- package/core/built/admin/assets/posts/{hooks-D4pJ_p_P.mjs → hooks-J2WpDj1d.mjs} +2 -2
- package/core/built/admin/assets/posts/{index-DzWTmJMy.mjs → index-D-vhKzUJ.mjs} +3 -3
- package/core/built/admin/assets/posts/{index-CDwlVw9E.mjs → index-LnPyGojr.mjs} +2 -2
- package/core/built/admin/assets/posts/{index-BuIZkUhD.mjs → index-nxfIbREV.mjs} +11 -11
- package/core/built/admin/assets/posts/{input-DUvSHOFz.mjs → input-DTtfruk_.mjs} +3 -3
- package/core/built/admin/assets/posts/{input-surface-CO8-j6V2.mjs → input-surface-DXTGCuo2.mjs} +2 -2
- package/core/built/admin/assets/posts/{koenig-lexical-cRm7bY-6.mjs → koenig-lexical-CNcifuT7.mjs} +2 -2
- package/core/built/admin/assets/posts/{kpis-BXukzbg6.mjs → kpis-n8L4BlMi.mjs} +5843 -5854
- package/core/built/admin/assets/posts/{label-TTt0QyDb.mjs → label-yl8B8ADj.mjs} +6 -6
- package/core/built/admin/assets/posts/{links-BAXELE82.mjs → links-DDApMJfk.mjs} +4 -4
- package/core/built/admin/assets/posts/{list-page-DQIiFvFP.mjs → list-page-BJJ9amzJ.mjs} +4 -4
- package/core/built/admin/assets/posts/{loading-indicator-BsY7eGY6.mjs → loading-indicator-CQBDNvdo.mjs} +3 -3
- package/core/built/admin/assets/posts/{mail-Bi1gt3MU.mjs → mail-BHg0SvNu.mjs} +2 -2
- package/core/built/admin/assets/posts/{main-layout-DejRuxP8.mjs → main-layout-DXveCJ5W.mjs} +2 -2
- package/core/built/admin/assets/posts/{newsletter-PCUtHW6Z.mjs → newsletter-DUpVmnKN.mjs} +19 -19
- package/core/built/admin/assets/posts/{overview-f4eDWazr.mjs → overview-sv6i-2h9.mjs} +18 -18
- package/core/built/admin/assets/posts/{pencil-DDnfXQqn.mjs → pencil-DsGDLJp6.mjs} +2 -2
- package/core/built/admin/assets/posts/{plus-DW3bG6ui.mjs → plus-DjZ_REvJ.mjs} +2 -2
- package/core/built/admin/assets/posts/{popover-Du2NcggM.mjs → popover-2PA9zdiD.mjs} +4 -4
- package/core/built/admin/assets/posts/{post-analytics-BmnHyWJq.mjs → post-analytics-CKHoiJFY.mjs} +6 -6
- package/core/built/admin/assets/posts/{post-analytics-context-0fieM91Y.mjs → post-analytics-context-D17CB7nm.mjs} +6 -6
- package/core/built/admin/assets/posts/{post-analytics-header-Bxi56AWV.mjs → post-analytics-header-aQLnrdWi.mjs} +12 -12
- package/core/built/admin/assets/posts/{post-share-modal-DiuT4GFO.mjs → post-share-modal-C8-8CZw8.mjs} +6 -6
- package/core/built/admin/assets/posts/{posts-CYjfxmgX.mjs → posts-BA2vu_T2.mjs} +2 -2
- package/core/built/admin/assets/posts/posts.js +1 -1
- package/core/built/admin/assets/posts/{search-Dz7F6ifc.mjs → search-Bx6BvCRk.mjs} +2 -2
- package/core/built/admin/assets/posts/{select-FX6QYAFU.mjs → select-QOJ2OvL0.mjs} +7 -7
- package/core/built/admin/assets/posts/{separator-C7QPCSjM.mjs → separator-CHtBa4OX.mjs} +3 -3
- package/core/built/admin/assets/posts/{settings-DJqx19W1.mjs → settings-CxhTCxXg.mjs} +2 -2
- package/core/built/admin/assets/posts/{sheet-wg_8-w45.mjs → sheet-DO5nhiru.mjs} +4 -4
- package/core/built/admin/assets/posts/{site-Bxv-L5Dn.mjs → site-6g4pKVnR.mjs} +2 -2
- package/core/built/admin/assets/posts/{skeleton-DrFcKwP3.mjs → skeleton-DgWx-52p.mjs} +3 -3
- package/core/built/admin/assets/posts/{source-icon-DD_bmIqg.mjs → source-icon-_zLbYeab.mjs} +4 -4
- package/core/built/admin/assets/posts/{stats-DkOg80Vt.mjs → stats-BGJFxx5f.mjs} +4 -4
- package/core/built/admin/assets/posts/{switch-DZFGAF_Y.mjs → switch-Djipx7gc.mjs} +5 -5
- package/core/built/admin/assets/posts/{table-CZCVviNy.mjs → table-DpDCE80J.mjs} +2 -2
- package/core/built/admin/assets/posts/{tabs-1fkhM4p8.mjs → tabs-CLWGiyQ4.mjs} +4 -4
- package/core/built/admin/assets/posts/{tags-CkyGqwqi.mjs → tags-B4KIn7er.mjs} +2 -2
- package/core/built/admin/assets/posts/{tags-BAmugIR1.mjs → tags-Dn1L_Z_4.mjs} +16 -16
- package/core/built/admin/assets/posts/{toggle-group-B_ubMZvo.mjs → toggle-group-DMMjXGB3.mjs} +4 -4
- package/core/built/admin/assets/posts/{tooltip-D1X0uVas.mjs → tooltip-DlCjZwmt.mjs} +5 -5
- package/core/built/admin/assets/posts/{underline-CV8OJ7qc.mjs → underline-WN3ZSFyI.mjs} +2 -2
- package/core/built/admin/assets/posts/{value-DhEK7_uT.mjs → value-BWiEmo45.mjs} +2 -2
- package/core/built/admin/assets/posts/{virtual-list-window-DqbQUkIo.mjs → virtual-list-window-F3Tvi9H5.mjs} +3 -3
- package/core/built/admin/assets/posts/{web-5jcYGeBz.mjs → web-C8PxkHvH.mjs} +18 -18
- package/core/built/admin/assets/posts/{x-Cl72IwQm.mjs → x-BdlZ3sG3.mjs} +2 -2
- package/core/built/admin/assets/posts/{zap-BPDyHF1u.mjs → zap-BuAk2C7n.mjs} +33 -21
- package/core/built/admin/assets/{posts-D6K0FKB9.js → posts-bRma2Aji.js} +1 -1
- package/core/built/admin/assets/power-B4VCYMPn.js +1 -0
- package/core/built/admin/assets/{referrers-C9UK_aF_.js → referrers-BgXy4ZDF.js} +1 -1
- package/core/built/admin/assets/{repeat-BwgUDBQn.js → repeat-BElPLZnh.js} +1 -1
- package/core/built/admin/assets/{reply-DbknkQhJ.js → reply-qlUFZ_G8.js} +1 -1
- package/core/built/admin/assets/{rocket-1EqU2MzS.js → rocket-CXYckFbm.js} +1 -1
- package/core/built/admin/assets/{select-BGJXWb8i.js → select-MGAxyhXP.js} +1 -1
- package/core/built/admin/assets/{send-_CqvCQBY.js → send-DcU5LcPI.js} +1 -1
- package/core/built/admin/assets/{settings-Bnep890F.js → settings-DGSAz4oa.js} +4 -4
- package/core/built/admin/assets/{settings-Ct244PHl.js → settings-DI95fXs5.js} +1 -1
- package/core/built/admin/assets/{share-modal-B8pN_3tk.js → share-modal-CsNAIPeN.js} +1 -1
- package/core/built/admin/assets/{sort-button-EXsVVSdw.js → sort-button-wV9PBd2P.js} +1 -1
- package/core/built/admin/assets/{source-icon-XMs0toB0.js → source-icon-CYt4X1-M.js} +1 -1
- package/core/built/admin/assets/{sprout-uA_pVmDr.js → sprout-5QqpAoO8.js} +1 -1
- package/core/built/admin/assets/{square-atKnWQyw.js → square-YHeYyKG9.js} +1 -1
- package/core/built/admin/assets/{stats-CPNnZGD9.js → stats-PD64opSw.js} +1 -1
- package/core/built/admin/assets/{stats-view-BP8LU5rf.js → stats-view-C4fnD3uG.js} +1 -1
- package/core/built/admin/assets/{step-1-FHeGC4GW.js → step-1-BqeM_LxC.js} +1 -1
- package/core/built/admin/assets/{step-2-BFhgD2N8.js → step-2-C7AcokE9.js} +1 -1
- package/core/built/admin/assets/{step-3-BZMKjY5n.js → step-3-9Z0qkfpM.js} +2 -2
- package/core/built/admin/assets/{table-kGA62bBZ.js → table-C8Mxw8fs.js} +1 -1
- package/core/built/admin/assets/{tabs-Dl3BXr4Z.js → tabs-2KXMNRG-.js} +1 -1
- package/core/built/admin/assets/{tags-CLhASxwZ.js → tags-Cx-Jx8Yb.js} +1 -1
- package/core/built/admin/assets/{tags-D-W0kJMe.js → tags-cn7wgGit.js} +1 -1
- package/core/built/admin/assets/{textarea-DMub6CsU.js → textarea-dKwIcAK5.js} +1 -1
- package/core/built/admin/assets/{thumbs-up-DZu4SX5N.js → thumbs-up-gzGza05L.js} +1 -1
- package/core/built/admin/assets/{tiers-2bg8BCFj.js → tiers-D--LgD9v.js} +1 -1
- package/core/built/admin/assets/{toggle-group-ByS0KZ3G.js → toggle-group-C8KbiBQS.js} +1 -1
- package/core/built/admin/assets/{topic-filter-oRqwlePZ.js → topic-filter--VQn4A7D.js} +1 -1
- package/core/built/admin/assets/{trash-BHoSD8mS.js → trash-CnBwlhjN.js} +1 -1
- package/core/built/admin/assets/{underline-1YSgOpPm.js → underline-B2FjHOE0.js} +1 -1
- package/core/built/admin/assets/{undo-2-BwYMlB8G.js → undo-2-fUPuO2Zt.js} +1 -1
- package/core/built/admin/assets/{upload-BDz0_o9b.js → upload-BPOrFdtp.js} +1 -1
- package/core/built/admin/assets/{use-growth-stats-PRVcCUT7.js → use-growth-stats-By0ZDqFi.js} +1 -1
- package/core/built/admin/assets/{use-pintura-config-CROsgSEd.js → use-pintura-config-0ykvCF7U.js} +1 -1
- package/core/built/admin/assets/{use-simple-pagination-CBjKBNNI.js → use-simple-pagination-TruuCHV6.js} +1 -1
- package/core/built/admin/assets/{user-round-check-DKooQiV7.js → user-round-check-C1Vahz0b.js} +1 -1
- package/core/built/admin/assets/{user-round-x-B94nAYJb.js → user-round-x-CzKFo5GY.js} +1 -1
- package/core/built/admin/assets/{virtual-list-window-Cf_vCQFb.js → virtual-list-window-BWEknk_A.js} +1 -1
- package/core/built/admin/assets/{wallet-cards-BorRQari.js → wallet-cards-CRHrrjiy.js} +1 -1
- package/core/built/admin/assets/{web-BVHy-80k.js → web-C2H0oAut.js} +1 -1
- package/core/built/admin/index.html +7 -7
- package/core/frontend/services/llms/service.js +37 -10
- package/core/frontend/services/routing/config.js +5 -3
- package/core/frontend/services/routing/config.ts +59 -0
- package/core/frontend/services/sitemap/handler.js +2 -14
- package/core/frontend/services/sitemap/site-map-manager.js +9 -214
- package/core/server/api/endpoints/comment-replies.js +1 -2
- package/core/server/api/endpoints/members.js +4 -1
- package/core/server/api/endpoints/search-index-public.js +12 -1
- package/core/server/api/endpoints/search-index.js +13 -2
- package/core/server/lib/bootstrap-socket.js +31 -36
- package/core/server/lib/bootstrap-socket.ts +114 -0
- package/core/server/lib/common/to-plain.js +9 -21
- package/core/server/lib/common/to-plain.ts +25 -0
- package/core/server/models/role-utils.js +5 -7
- package/core/server/models/role-utils.ts +56 -0
- package/core/server/services/adapter-manager/adapter-manager.js +5 -1
- package/core/server/services/audience-feedback/audience-feedback-service.js +1 -1
- package/core/server/services/automations/automations-api.js +37 -2
- package/core/server/services/automations/automations-api.ts +43 -2
- package/core/server/services/comments/comments-controller.js +87 -55
- package/core/server/services/comments/comments-service-emails.js +1 -1
- package/core/server/services/comments/comments-service.js +249 -79
- package/core/server/services/mail/templates/notification.html +157 -0
- package/core/server/services/member-attribution/url-translator.js +1 -1
- package/core/server/services/members/members-api/services/token-service.js +21 -2
- package/core/server/services/notifications/notification-email.js +38 -0
- package/core/server/services/notifications/notification-email.ts +66 -0
- package/core/server/services/notifications/sanitize-email-html.js +37 -0
- package/core/server/services/notifications/sanitize-email-html.ts +35 -0
- package/core/server/services/route-settings/route-settings.js +2 -18
- package/core/server/services/route-settings/validate.js +2 -1
- package/core/server/services/update-check/index.js +10 -3
- package/core/server/services/update-check/update-check-service.js +18 -25
- package/core/server/services/url/index.js +1 -16
- package/core/shared/config/defaults.json +0 -1
- package/package.json +2 -1
- package/pnpm-lock.yaml +11 -1
- package/pnpm-workspace.yaml +2 -0
- package/core/built/admin/assets/editor-G3BxjP48.js +0 -8
- package/core/built/admin/assets/gh-chart-vpcpdiS9.js +0 -1
- package/core/built/admin/assets/index-BP5ial3p.js +0 -1
- package/core/built/admin/assets/index-Bd9VZ28k.css +0 -1
- package/core/built/admin/assets/kpis-B8pv3wne.js +0 -1
- package/core/built/admin/assets/label-BjHICZC1.js +0 -1
- package/core/built/admin/assets/members-DcuuSs_v.js +0 -10
- package/core/built/admin/assets/minus-Dv51QEND.js +0 -1
- package/core/built/admin/assets/offers-DKZ0qsmb.js +0 -1
- package/core/built/admin/assets/power-D5QO6MjY.js +0 -1
- package/core/server/services/url/lazy-find-resource.js +0 -49
- package/core/server/services/url/lazy-find-resource.ts +0 -62
- package/core/server/services/url/lazy-url-service.js +0 -262
- package/core/server/services/url/lazy-url-service.ts +0 -308
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.LazyUrlService = void 0;
|
|
4
|
-
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
5
|
-
const nql = require('@tryghost/nql');
|
|
6
|
-
const debug = require('@tryghost/debug')('services:url:lazy');
|
|
7
|
-
const logging = require('@tryghost/logging');
|
|
8
|
-
const errors = require('@tryghost/errors');
|
|
9
|
-
const localUtils = require('../../../shared/url-utils');
|
|
10
|
-
// The same expansions used by UrlGenerator so users can write `tag:foo`,
|
|
11
|
-
// `author:jane`, etc. and have them rewritten to the underlying field paths.
|
|
12
|
-
const EXPANSIONS = [
|
|
13
|
-
{ key: 'author', replacement: 'authors.slug' },
|
|
14
|
-
{ key: 'tags', replacement: 'tags.slug' },
|
|
15
|
-
{ key: 'tag', replacement: 'tags.slug' },
|
|
16
|
-
{ key: 'authors', replacement: 'authors.slug' },
|
|
17
|
-
{ key: 'primary_tag', replacement: 'primary_tag.slug' },
|
|
18
|
-
{ key: 'primary_author', replacement: 'primary_author.slug' }
|
|
19
|
-
];
|
|
20
|
-
// Same `page:true/false` legacy transformer the UrlGenerator uses, so old
|
|
21
|
-
// routes.yaml configs continue to work.
|
|
22
|
-
const PAGE_TRANSFORMER = nql.utils.mapKeyValues({
|
|
23
|
-
key: { from: 'page', to: 'type' },
|
|
24
|
-
values: [
|
|
25
|
-
{ from: false, to: 'post' },
|
|
26
|
-
{ from: true, to: 'page' }
|
|
27
|
-
]
|
|
28
|
-
});
|
|
29
|
-
// Map a resource's `type` field (whatever the caller passed) onto the plural
|
|
30
|
-
// router resource type. We accept the singular DB column values ('post',
|
|
31
|
-
// 'page') as well as the plural router keys, because the migrated callers
|
|
32
|
-
// are inconsistent: API responses spread `attrs` (singular), but some
|
|
33
|
-
// helpers explicitly tag with the plural router key.
|
|
34
|
-
const TYPE_TO_ROUTER_TYPE = {
|
|
35
|
-
post: 'posts',
|
|
36
|
-
posts: 'posts',
|
|
37
|
-
page: 'pages',
|
|
38
|
-
pages: 'pages',
|
|
39
|
-
tag: 'tags',
|
|
40
|
-
tags: 'tags',
|
|
41
|
-
author: 'authors',
|
|
42
|
-
authors: 'authors'
|
|
43
|
-
};
|
|
44
|
-
/**
|
|
45
|
-
* On-demand URL service. Computes URLs and ownership per-call from the
|
|
46
|
-
* registered router configs instead of holding a precomputed map of every
|
|
47
|
-
* resource. Pure for forward lookups; resolveUrl() takes an optional DB
|
|
48
|
-
* lookup function so reverse lookups stay testable.
|
|
49
|
-
*/
|
|
50
|
-
class LazyUrlService {
|
|
51
|
-
urlUtils;
|
|
52
|
-
findResource;
|
|
53
|
-
routerConfigs;
|
|
54
|
-
constructor({ urlUtils = localUtils, findResource = null } = {}) {
|
|
55
|
-
this.urlUtils = urlUtils;
|
|
56
|
-
this.findResource = findResource;
|
|
57
|
-
// Router configs in priority order. Position is the registration order.
|
|
58
|
-
this.routerConfigs = [];
|
|
59
|
-
}
|
|
60
|
-
onRouterAddedType(identifier, filter, resourceType, permalink) {
|
|
61
|
-
debug('onRouterAddedType', identifier, resourceType, permalink, filter);
|
|
62
|
-
const config = { identifier, filter, resourceType, permalink, nql: null };
|
|
63
|
-
if (filter) {
|
|
64
|
-
config.nql = nql(filter, { expansions: EXPANSIONS, transformer: PAGE_TRANSFORMER });
|
|
65
|
-
}
|
|
66
|
-
this.routerConfigs.push(config);
|
|
67
|
-
}
|
|
68
|
-
onRouterUpdated() {
|
|
69
|
-
// No state to regenerate. The next URL request reads the (possibly new)
|
|
70
|
-
// router config; in-flight requests that already snapshotted the old
|
|
71
|
-
// config keep using it, which matches the documented atomic-reload
|
|
72
|
-
// behaviour.
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Drop all registered routers. Called when routes.yaml is reloaded.
|
|
76
|
-
*/
|
|
77
|
-
reset() {
|
|
78
|
-
this.routerConfigs = [];
|
|
79
|
-
}
|
|
80
|
-
hasFinished() {
|
|
81
|
-
return true;
|
|
82
|
-
}
|
|
83
|
-
getUrlForResource(resource, options = {}) {
|
|
84
|
-
const routerType = this._routerTypeOf(resource);
|
|
85
|
-
if (!routerType) {
|
|
86
|
-
return this._formatPath('/404/', options);
|
|
87
|
-
}
|
|
88
|
-
const candidates = this.routerConfigs.filter(c => c.resourceType === routerType);
|
|
89
|
-
for (const config of candidates) {
|
|
90
|
-
// Warn loudly when a caller passes a resource that's missing
|
|
91
|
-
// the relations a tag- or author-filtered router needs. The
|
|
92
|
-
// URL silently falls through to /404/ on those routes; the
|
|
93
|
-
// log lets operators alert on any caller that hasn't been
|
|
94
|
-
// inflated with withRelated: ['tags', 'authors'].
|
|
95
|
-
this._warnIfThin(config, resource, routerType);
|
|
96
|
-
// NQL filters are evaluated against the original resource (with
|
|
97
|
-
// its singular DB `type` field intact) because the page:true/false
|
|
98
|
-
// transformer rewrites to type:'post'/'page'.
|
|
99
|
-
if (this._matchesFilter(config, resource)) {
|
|
100
|
-
const path = this.urlUtils.replacePermalink(config.permalink, resource);
|
|
101
|
-
return this._formatPath(path, options);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
return this._formatPath('/404/', options);
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Warn when a caller passes a thin resource (missing tags/authors)
|
|
108
|
-
* against a router whose filter references those relations. The
|
|
109
|
-
* filter eval will fail silently and the URL will resolve to /404/.
|
|
110
|
-
* Detection is conservative: only fires when the router filter
|
|
111
|
-
* actually references the relation, so a tag-less router (`filter:
|
|
112
|
-
* featured:true`) doesn't log for a tag-less resource.
|
|
113
|
-
*/
|
|
114
|
-
_warnIfThin(config, resource, routerType) {
|
|
115
|
-
if (!config.filter) {
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
const needsTags = /\btags?\b|\bprimary_tag\b/.test(config.filter);
|
|
119
|
-
const needsAuthors = /\bauthors?\b|\bprimary_author\b/.test(config.filter);
|
|
120
|
-
const r = resource;
|
|
121
|
-
const missingTags = needsTags && !Array.isArray(r.tags);
|
|
122
|
-
const missingAuthors = needsAuthors && !Array.isArray(r.authors);
|
|
123
|
-
if (!missingTags && !missingAuthors) {
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
const missing = [missingTags && 'tags', missingAuthors && 'authors']
|
|
127
|
-
.filter(Boolean);
|
|
128
|
-
logging.error(new errors.InternalServerError({
|
|
129
|
-
message: 'Thin resource passed to LazyUrlService.getUrlForResource',
|
|
130
|
-
code: 'LAZY_URL_THIN_RESOURCE',
|
|
131
|
-
errorDetails: {
|
|
132
|
-
resourceType: routerType,
|
|
133
|
-
resourceId: resource.id,
|
|
134
|
-
routerIdentifier: config.identifier,
|
|
135
|
-
filter: config.filter,
|
|
136
|
-
missing
|
|
137
|
-
}
|
|
138
|
-
}));
|
|
139
|
-
}
|
|
140
|
-
ownsResource(routerIdentifier, resource) {
|
|
141
|
-
const config = this.routerConfigs.find(c => c.identifier === routerIdentifier);
|
|
142
|
-
if (!config || !resource) {
|
|
143
|
-
return false;
|
|
144
|
-
}
|
|
145
|
-
const routerType = this._routerTypeOf(resource);
|
|
146
|
-
if (config.resourceType !== routerType) {
|
|
147
|
-
return false;
|
|
148
|
-
}
|
|
149
|
-
return this._matchesFilter(config, resource);
|
|
150
|
-
}
|
|
151
|
-
/**
|
|
152
|
-
* Translate the resource's `type` field into the plural router resource
|
|
153
|
-
* type (`'posts'` / `'pages'` / `'tags'` / `'authors'`). Returns `null`
|
|
154
|
-
* when the type is missing or unrecognised.
|
|
155
|
-
*/
|
|
156
|
-
_routerTypeOf(resource) {
|
|
157
|
-
if (!resource || !resource.type) {
|
|
158
|
-
return null;
|
|
159
|
-
}
|
|
160
|
-
return TYPE_TO_ROUTER_TYPE[resource.type] || null;
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* Resolve a URL path to a resource record. Iterates router configs in
|
|
164
|
-
* priority order, pattern-matching the path against each permalink
|
|
165
|
-
* template, querying the database for a matching resource, and verifying
|
|
166
|
-
* any NQL filter still matches for posts.
|
|
167
|
-
*/
|
|
168
|
-
async resolveUrl(urlPath) {
|
|
169
|
-
if (!this.findResource) {
|
|
170
|
-
return null;
|
|
171
|
-
}
|
|
172
|
-
for (const config of this.routerConfigs) {
|
|
173
|
-
const params = this._matchPermalink(config.permalink, urlPath);
|
|
174
|
-
if (!params) {
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
const resource = await this.findResource(config.resourceType, params);
|
|
178
|
-
if (!resource) {
|
|
179
|
-
continue;
|
|
180
|
-
}
|
|
181
|
-
// For posts/pages with NQL filters, confirm the DB record still
|
|
182
|
-
// satisfies the filter. The match runs against the raw record
|
|
183
|
-
// (with its singular `type` column) because the page:true/false
|
|
184
|
-
// transformer rewrites to type:'post'/'page'.
|
|
185
|
-
if (config.nql && !this._matchesFilter(config, resource)) {
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
188
|
-
return Object.assign({}, resource, { type: config.resourceType });
|
|
189
|
-
}
|
|
190
|
-
return null;
|
|
191
|
-
}
|
|
192
|
-
_matchesFilter(config, resource) {
|
|
193
|
-
if (!config.nql) {
|
|
194
|
-
return true;
|
|
195
|
-
}
|
|
196
|
-
try {
|
|
197
|
-
return !!config.nql.queryJSON(resource);
|
|
198
|
-
}
|
|
199
|
-
catch (err) {
|
|
200
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
201
|
-
debug('NQL match failed', config.filter, message);
|
|
202
|
-
return false;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
_formatPath(path, options) {
|
|
206
|
-
if (options.absolute) {
|
|
207
|
-
return this.urlUtils.createUrl(path, options.absolute);
|
|
208
|
-
}
|
|
209
|
-
if (options.withSubdirectory) {
|
|
210
|
-
return this.urlUtils.createUrl(path, false, true);
|
|
211
|
-
}
|
|
212
|
-
return path;
|
|
213
|
-
}
|
|
214
|
-
/**
|
|
215
|
-
* Match a Ghost permalink template (e.g. `/:slug/`,
|
|
216
|
-
* `/:year/:month/:slug/`) against a URL path and extract the named
|
|
217
|
-
* fields. Ghost's RouteSettings validator rewrites `{field}` placeholders
|
|
218
|
-
* into `:field` form before they reach the URL service, so this parser
|
|
219
|
-
* works on the `:field` syntax.
|
|
220
|
-
*
|
|
221
|
-
* Implementation: walk the permalink and path one segment at a time.
|
|
222
|
-
* Each segment must be either a literal match or a single placeholder.
|
|
223
|
-
* Mixing literals and placeholders inside a segment is unsupported and
|
|
224
|
-
* not a documented Ghost feature; rejecting it here also keeps us out of
|
|
225
|
-
* regex-backtracking territory entirely.
|
|
226
|
-
*/
|
|
227
|
-
_matchPermalink(permalink, urlPath) {
|
|
228
|
-
if (typeof permalink !== 'string' || typeof urlPath !== 'string') {
|
|
229
|
-
return null;
|
|
230
|
-
}
|
|
231
|
-
const permalinkSegments = permalink.split('/');
|
|
232
|
-
const pathSegments = urlPath.split('/');
|
|
233
|
-
if (permalinkSegments.length !== pathSegments.length) {
|
|
234
|
-
return null;
|
|
235
|
-
}
|
|
236
|
-
const params = {};
|
|
237
|
-
for (let i = 0; i < permalinkSegments.length; i += 1) {
|
|
238
|
-
const templateSegment = permalinkSegments[i];
|
|
239
|
-
const pathSegment = pathSegments[i];
|
|
240
|
-
if (templateSegment.startsWith(':')) {
|
|
241
|
-
const fieldName = templateSegment.slice(1);
|
|
242
|
-
if (!/^\w+$/.test(fieldName) || pathSegment.length === 0) {
|
|
243
|
-
return null;
|
|
244
|
-
}
|
|
245
|
-
try {
|
|
246
|
-
params[fieldName] = decodeURIComponent(pathSegment);
|
|
247
|
-
}
|
|
248
|
-
catch {
|
|
249
|
-
// Malformed %-escapes (e.g. "/foo%ZZ/") throw URIError;
|
|
250
|
-
// treat as a non-match rather than crash the request.
|
|
251
|
-
return null;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
else if (templateSegment !== pathSegment) {
|
|
255
|
-
return null;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
return params;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
exports.LazyUrlService = LazyUrlService;
|
|
262
|
-
module.exports = LazyUrlService;
|
|
@@ -1,308 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
2
|
-
const nql = require('@tryghost/nql');
|
|
3
|
-
const debug = require('@tryghost/debug')('services:url:lazy');
|
|
4
|
-
const logging = require('@tryghost/logging');
|
|
5
|
-
const errors = require('@tryghost/errors');
|
|
6
|
-
const localUtils = require('../../../shared/url-utils');
|
|
7
|
-
/* eslint-enable @typescript-eslint/no-require-imports */
|
|
8
|
-
|
|
9
|
-
import type {Resource, UrlOptions, LazyUrlServiceBackend} from './url-service-facade';
|
|
10
|
-
|
|
11
|
-
// The same expansions used by UrlGenerator so users can write `tag:foo`,
|
|
12
|
-
// `author:jane`, etc. and have them rewritten to the underlying field paths.
|
|
13
|
-
const EXPANSIONS = [
|
|
14
|
-
{key: 'author', replacement: 'authors.slug'},
|
|
15
|
-
{key: 'tags', replacement: 'tags.slug'},
|
|
16
|
-
{key: 'tag', replacement: 'tags.slug'},
|
|
17
|
-
{key: 'authors', replacement: 'authors.slug'},
|
|
18
|
-
{key: 'primary_tag', replacement: 'primary_tag.slug'},
|
|
19
|
-
{key: 'primary_author', replacement: 'primary_author.slug'}
|
|
20
|
-
];
|
|
21
|
-
|
|
22
|
-
// Same `page:true/false` legacy transformer the UrlGenerator uses, so old
|
|
23
|
-
// routes.yaml configs continue to work.
|
|
24
|
-
const PAGE_TRANSFORMER = nql.utils.mapKeyValues({
|
|
25
|
-
key: {from: 'page', to: 'type'},
|
|
26
|
-
values: [
|
|
27
|
-
{from: false, to: 'post'},
|
|
28
|
-
{from: true, to: 'page'}
|
|
29
|
-
]
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
// Map a resource's `type` field (whatever the caller passed) onto the plural
|
|
33
|
-
// router resource type. We accept the singular DB column values ('post',
|
|
34
|
-
// 'page') as well as the plural router keys, because the migrated callers
|
|
35
|
-
// are inconsistent: API responses spread `attrs` (singular), but some
|
|
36
|
-
// helpers explicitly tag with the plural router key.
|
|
37
|
-
const TYPE_TO_ROUTER_TYPE: Record<string, string> = {
|
|
38
|
-
post: 'posts',
|
|
39
|
-
posts: 'posts',
|
|
40
|
-
page: 'pages',
|
|
41
|
-
pages: 'pages',
|
|
42
|
-
tag: 'tags',
|
|
43
|
-
tags: 'tags',
|
|
44
|
-
author: 'authors',
|
|
45
|
-
authors: 'authors'
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
interface NqlInstance {
|
|
49
|
-
queryJSON(record: Record<string, unknown>): unknown;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
interface RouterConfig {
|
|
53
|
-
identifier: string;
|
|
54
|
-
filter: string | null;
|
|
55
|
-
resourceType: string;
|
|
56
|
-
permalink: string;
|
|
57
|
-
nql: NqlInstance | null;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Async function that resolves a slug (or other params) to a database
|
|
62
|
-
* record. Injected so the URL service stays pure for forward lookups —
|
|
63
|
-
* only `resolveUrl` actually hits the DB, and only via this hook.
|
|
64
|
-
*/
|
|
65
|
-
export type FindResource = (
|
|
66
|
-
routerType: string,
|
|
67
|
-
params: Record<string, string>
|
|
68
|
-
) => Promise<Record<string, unknown> | null>;
|
|
69
|
-
|
|
70
|
-
interface LazyUrlServiceDeps {
|
|
71
|
-
urlUtils?: typeof localUtils;
|
|
72
|
-
findResource?: FindResource | null;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* On-demand URL service. Computes URLs and ownership per-call from the
|
|
77
|
-
* registered router configs instead of holding a precomputed map of every
|
|
78
|
-
* resource. Pure for forward lookups; resolveUrl() takes an optional DB
|
|
79
|
-
* lookup function so reverse lookups stay testable.
|
|
80
|
-
*/
|
|
81
|
-
export class LazyUrlService implements LazyUrlServiceBackend {
|
|
82
|
-
private urlUtils: typeof localUtils;
|
|
83
|
-
private findResource: FindResource | null;
|
|
84
|
-
private routerConfigs: RouterConfig[];
|
|
85
|
-
|
|
86
|
-
constructor({urlUtils = localUtils, findResource = null}: LazyUrlServiceDeps = {}) {
|
|
87
|
-
this.urlUtils = urlUtils;
|
|
88
|
-
this.findResource = findResource;
|
|
89
|
-
// Router configs in priority order. Position is the registration order.
|
|
90
|
-
this.routerConfigs = [];
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
onRouterAddedType(
|
|
94
|
-
identifier: string,
|
|
95
|
-
filter: string | null,
|
|
96
|
-
resourceType: string,
|
|
97
|
-
permalink: string
|
|
98
|
-
): void {
|
|
99
|
-
debug('onRouterAddedType', identifier, resourceType, permalink, filter);
|
|
100
|
-
const config: RouterConfig = {identifier, filter, resourceType, permalink, nql: null};
|
|
101
|
-
if (filter) {
|
|
102
|
-
config.nql = nql(filter, {expansions: EXPANSIONS, transformer: PAGE_TRANSFORMER});
|
|
103
|
-
}
|
|
104
|
-
this.routerConfigs.push(config);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
onRouterUpdated(): void {
|
|
108
|
-
// No state to regenerate. The next URL request reads the (possibly new)
|
|
109
|
-
// router config; in-flight requests that already snapshotted the old
|
|
110
|
-
// config keep using it, which matches the documented atomic-reload
|
|
111
|
-
// behaviour.
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Drop all registered routers. Called when routes.yaml is reloaded.
|
|
116
|
-
*/
|
|
117
|
-
reset(): void {
|
|
118
|
-
this.routerConfigs = [];
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
hasFinished(): boolean {
|
|
122
|
-
return true;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
getUrlForResource(resource: Resource, options: UrlOptions = {}): string {
|
|
126
|
-
const routerType = this._routerTypeOf(resource);
|
|
127
|
-
if (!routerType) {
|
|
128
|
-
return this._formatPath('/404/', options);
|
|
129
|
-
}
|
|
130
|
-
const candidates = this.routerConfigs.filter(c => c.resourceType === routerType);
|
|
131
|
-
for (const config of candidates) {
|
|
132
|
-
// Warn loudly when a caller passes a resource that's missing
|
|
133
|
-
// the relations a tag- or author-filtered router needs. The
|
|
134
|
-
// URL silently falls through to /404/ on those routes; the
|
|
135
|
-
// log lets operators alert on any caller that hasn't been
|
|
136
|
-
// inflated with withRelated: ['tags', 'authors'].
|
|
137
|
-
this._warnIfThin(config, resource, routerType);
|
|
138
|
-
// NQL filters are evaluated against the original resource (with
|
|
139
|
-
// its singular DB `type` field intact) because the page:true/false
|
|
140
|
-
// transformer rewrites to type:'post'/'page'.
|
|
141
|
-
if (this._matchesFilter(config, resource)) {
|
|
142
|
-
const path = this.urlUtils.replacePermalink(config.permalink, resource);
|
|
143
|
-
return this._formatPath(path, options);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
return this._formatPath('/404/', options);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Warn when a caller passes a thin resource (missing tags/authors)
|
|
151
|
-
* against a router whose filter references those relations. The
|
|
152
|
-
* filter eval will fail silently and the URL will resolve to /404/.
|
|
153
|
-
* Detection is conservative: only fires when the router filter
|
|
154
|
-
* actually references the relation, so a tag-less router (`filter:
|
|
155
|
-
* featured:true`) doesn't log for a tag-less resource.
|
|
156
|
-
*/
|
|
157
|
-
private _warnIfThin(config: RouterConfig, resource: Resource, routerType: string): void {
|
|
158
|
-
if (!config.filter) {
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
const needsTags = /\btags?\b|\bprimary_tag\b/.test(config.filter);
|
|
162
|
-
const needsAuthors = /\bauthors?\b|\bprimary_author\b/.test(config.filter);
|
|
163
|
-
const r = resource as Record<string, unknown>;
|
|
164
|
-
const missingTags = needsTags && !Array.isArray(r.tags);
|
|
165
|
-
const missingAuthors = needsAuthors && !Array.isArray(r.authors);
|
|
166
|
-
if (!missingTags && !missingAuthors) {
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
const missing = [missingTags && 'tags', missingAuthors && 'authors']
|
|
170
|
-
.filter(Boolean) as string[];
|
|
171
|
-
logging.error(new errors.InternalServerError({
|
|
172
|
-
message: 'Thin resource passed to LazyUrlService.getUrlForResource',
|
|
173
|
-
code: 'LAZY_URL_THIN_RESOURCE',
|
|
174
|
-
errorDetails: {
|
|
175
|
-
resourceType: routerType,
|
|
176
|
-
resourceId: resource.id,
|
|
177
|
-
routerIdentifier: config.identifier,
|
|
178
|
-
filter: config.filter,
|
|
179
|
-
missing
|
|
180
|
-
}
|
|
181
|
-
}));
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
ownsResource(routerIdentifier: string, resource: Resource | null): boolean {
|
|
185
|
-
const config = this.routerConfigs.find(c => c.identifier === routerIdentifier);
|
|
186
|
-
if (!config || !resource) {
|
|
187
|
-
return false;
|
|
188
|
-
}
|
|
189
|
-
const routerType = this._routerTypeOf(resource);
|
|
190
|
-
if (config.resourceType !== routerType) {
|
|
191
|
-
return false;
|
|
192
|
-
}
|
|
193
|
-
return this._matchesFilter(config, resource);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Translate the resource's `type` field into the plural router resource
|
|
198
|
-
* type (`'posts'` / `'pages'` / `'tags'` / `'authors'`). Returns `null`
|
|
199
|
-
* when the type is missing or unrecognised.
|
|
200
|
-
*/
|
|
201
|
-
private _routerTypeOf(resource: Resource | null | undefined): string | null {
|
|
202
|
-
if (!resource || !resource.type) {
|
|
203
|
-
return null;
|
|
204
|
-
}
|
|
205
|
-
return TYPE_TO_ROUTER_TYPE[resource.type] || null;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Resolve a URL path to a resource record. Iterates router configs in
|
|
210
|
-
* priority order, pattern-matching the path against each permalink
|
|
211
|
-
* template, querying the database for a matching resource, and verifying
|
|
212
|
-
* any NQL filter still matches for posts.
|
|
213
|
-
*/
|
|
214
|
-
async resolveUrl(urlPath: string): Promise<Resource | null> {
|
|
215
|
-
if (!this.findResource) {
|
|
216
|
-
return null;
|
|
217
|
-
}
|
|
218
|
-
for (const config of this.routerConfigs) {
|
|
219
|
-
const params = this._matchPermalink(config.permalink, urlPath);
|
|
220
|
-
if (!params) {
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
223
|
-
const resource = await this.findResource(config.resourceType, params);
|
|
224
|
-
if (!resource) {
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
// For posts/pages with NQL filters, confirm the DB record still
|
|
228
|
-
// satisfies the filter. The match runs against the raw record
|
|
229
|
-
// (with its singular `type` column) because the page:true/false
|
|
230
|
-
// transformer rewrites to type:'post'/'page'.
|
|
231
|
-
if (config.nql && !this._matchesFilter(config, resource)) {
|
|
232
|
-
continue;
|
|
233
|
-
}
|
|
234
|
-
return Object.assign({}, resource, {type: config.resourceType}) as Resource;
|
|
235
|
-
}
|
|
236
|
-
return null;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
private _matchesFilter(config: RouterConfig, resource: Record<string, unknown>): boolean {
|
|
240
|
-
if (!config.nql) {
|
|
241
|
-
return true;
|
|
242
|
-
}
|
|
243
|
-
try {
|
|
244
|
-
return !!config.nql.queryJSON(resource);
|
|
245
|
-
} catch (err) {
|
|
246
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
247
|
-
debug('NQL match failed', config.filter, message);
|
|
248
|
-
return false;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
private _formatPath(path: string, options: UrlOptions): string {
|
|
253
|
-
if (options.absolute) {
|
|
254
|
-
return this.urlUtils.createUrl(path, options.absolute);
|
|
255
|
-
}
|
|
256
|
-
if (options.withSubdirectory) {
|
|
257
|
-
return this.urlUtils.createUrl(path, false, true);
|
|
258
|
-
}
|
|
259
|
-
return path;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Match a Ghost permalink template (e.g. `/:slug/`,
|
|
264
|
-
* `/:year/:month/:slug/`) against a URL path and extract the named
|
|
265
|
-
* fields. Ghost's RouteSettings validator rewrites `{field}` placeholders
|
|
266
|
-
* into `:field` form before they reach the URL service, so this parser
|
|
267
|
-
* works on the `:field` syntax.
|
|
268
|
-
*
|
|
269
|
-
* Implementation: walk the permalink and path one segment at a time.
|
|
270
|
-
* Each segment must be either a literal match or a single placeholder.
|
|
271
|
-
* Mixing literals and placeholders inside a segment is unsupported and
|
|
272
|
-
* not a documented Ghost feature; rejecting it here also keeps us out of
|
|
273
|
-
* regex-backtracking territory entirely.
|
|
274
|
-
*/
|
|
275
|
-
private _matchPermalink(permalink: string, urlPath: string): Record<string, string> | null {
|
|
276
|
-
if (typeof permalink !== 'string' || typeof urlPath !== 'string') {
|
|
277
|
-
return null;
|
|
278
|
-
}
|
|
279
|
-
const permalinkSegments = permalink.split('/');
|
|
280
|
-
const pathSegments = urlPath.split('/');
|
|
281
|
-
if (permalinkSegments.length !== pathSegments.length) {
|
|
282
|
-
return null;
|
|
283
|
-
}
|
|
284
|
-
const params: Record<string, string> = {};
|
|
285
|
-
for (let i = 0; i < permalinkSegments.length; i += 1) {
|
|
286
|
-
const templateSegment = permalinkSegments[i];
|
|
287
|
-
const pathSegment = pathSegments[i];
|
|
288
|
-
if (templateSegment.startsWith(':')) {
|
|
289
|
-
const fieldName = templateSegment.slice(1);
|
|
290
|
-
if (!/^\w+$/.test(fieldName) || pathSegment.length === 0) {
|
|
291
|
-
return null;
|
|
292
|
-
}
|
|
293
|
-
try {
|
|
294
|
-
params[fieldName] = decodeURIComponent(pathSegment);
|
|
295
|
-
} catch {
|
|
296
|
-
// Malformed %-escapes (e.g. "/foo%ZZ/") throw URIError;
|
|
297
|
-
// treat as a non-match rather than crash the request.
|
|
298
|
-
return null;
|
|
299
|
-
}
|
|
300
|
-
} else if (templateSegment !== pathSegment) {
|
|
301
|
-
return null;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
return params;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
module.exports = LazyUrlService;
|