emdash 0.15.0 → 0.16.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/dist/api/route-utils.mjs +10 -10
- package/dist/api/schemas/index.d.mts +1 -1
- package/dist/{api-CLwG_3dh.mjs → api-BNKqxyFX.mjs} +54 -14
- package/dist/{api-CLwG_3dh.mjs.map → api-BNKqxyFX.mjs.map} +1 -1
- package/dist/{apply-wJhM_bwU.mjs → apply-BOPaD-s9.mjs} +16 -16
- package/dist/{apply-wJhM_bwU.mjs.map → apply-BOPaD-s9.mjs.map} +1 -1
- package/dist/astro/index.d.mts +3 -3
- package/dist/astro/index.d.mts.map +1 -1
- package/dist/astro/index.mjs +33 -1
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +3 -3
- package/dist/astro/middleware/auth.mjs +2 -2
- package/dist/astro/middleware/redirect.mjs +4 -4
- package/dist/astro/middleware/request-context.mjs +1 -1
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +66 -46
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +3 -3
- package/dist/astro/routes/api/admin/allowed-domains/index.mjs +3 -3
- package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +2 -2
- package/dist/astro/routes/api/admin/api-tokens/index.mjs +3 -3
- package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +8 -8
- package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +9 -9
- package/dist/astro/routes/api/admin/bylines/index.mjs +9 -9
- package/dist/astro/routes/api/admin/comments/_id_/status.mjs +7 -7
- package/dist/astro/routes/api/admin/comments/_id_.mjs +5 -5
- package/dist/astro/routes/api/admin/comments/bulk.mjs +6 -6
- package/dist/astro/routes/api/admin/comments/counts.mjs +5 -5
- package/dist/astro/routes/api/admin/comments/index.mjs +6 -6
- package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +4 -4
- package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +3 -3
- package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +3 -3
- package/dist/astro/routes/api/admin/oauth-clients/index.mjs +3 -3
- package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +27 -27
- package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +27 -27
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +27 -27
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +27 -27
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +27 -27
- package/dist/astro/routes/api/admin/plugins/index.mjs +27 -27
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +3 -3
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +27 -27
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +27 -27
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +27 -27
- package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +27 -27
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +41 -28
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/artifact.d.mts +8 -0
- package/dist/astro/routes/api/admin/plugins/registry/artifact.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +301 -0
- package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs.map +1 -0
- package/dist/astro/routes/api/admin/plugins/registry/install.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/install.mjs +46 -28
- package/dist/astro/routes/api/admin/plugins/registry/install.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/updates.mjs +27 -27
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +27 -27
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +3 -3
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +27 -27
- package/dist/astro/routes/api/admin/users/_id_/disable.mjs +2 -2
- package/dist/astro/routes/api/admin/users/_id_/enable.mjs +2 -2
- package/dist/astro/routes/api/admin/users/_id_/index.mjs +3 -3
- package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +2 -2
- package/dist/astro/routes/api/admin/users/index.mjs +3 -3
- package/dist/astro/routes/api/auth/dev-bypass.mjs +3 -3
- package/dist/astro/routes/api/auth/invite/accept.mjs +2 -2
- package/dist/astro/routes/api/auth/invite/complete.mjs +3 -3
- package/dist/astro/routes/api/auth/invite/index.mjs +3 -3
- package/dist/astro/routes/api/auth/invite/register-options.mjs +3 -3
- package/dist/astro/routes/api/auth/logout.mjs +2 -2
- package/dist/astro/routes/api/auth/magic-link/send.mjs +4 -4
- package/dist/astro/routes/api/auth/magic-link/verify.mjs +2 -2
- package/dist/astro/routes/api/auth/me.mjs +3 -3
- package/dist/astro/routes/api/auth/passkey/_id_.mjs +3 -3
- package/dist/astro/routes/api/auth/passkey/index.mjs +2 -2
- package/dist/astro/routes/api/auth/passkey/options.mjs +4 -4
- package/dist/astro/routes/api/auth/passkey/register/options.mjs +3 -3
- package/dist/astro/routes/api/auth/passkey/register/verify.mjs +3 -3
- package/dist/astro/routes/api/auth/passkey/verify.mjs +3 -3
- package/dist/astro/routes/api/auth/signup/complete.mjs +3 -3
- package/dist/astro/routes/api/auth/signup/request.mjs +4 -4
- package/dist/astro/routes/api/auth/signup/verify.mjs +2 -2
- package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +6 -6
- package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +4 -4
- package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +4 -4
- package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +4 -4
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +8 -8
- package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_.mjs +4 -4
- package/dist/astro/routes/api/content/_collection_/index.mjs +4 -4
- package/dist/astro/routes/api/content/_collection_/trash.mjs +4 -4
- package/dist/astro/routes/api/dashboard.mjs +7 -7
- package/dist/astro/routes/api/dev/emails.mjs +2 -2
- package/dist/astro/routes/api/import/probe.mjs +4 -4
- package/dist/astro/routes/api/import/wordpress/analyze.mjs +3 -3
- package/dist/astro/routes/api/import/wordpress/execute.d.mts +3 -3
- package/dist/astro/routes/api/import/wordpress/execute.mjs +8 -8
- package/dist/astro/routes/api/import/wordpress/media.mjs +4 -4
- package/dist/astro/routes/api/import/wordpress/prepare.mjs +6 -6
- package/dist/astro/routes/api/import/wordpress/rewrite-url-helpers.d.mts +11 -1
- package/dist/astro/routes/api/import/wordpress/rewrite-url-helpers.d.mts.map +1 -1
- package/dist/astro/routes/api/import/wordpress/rewrite-url-helpers.mjs +17 -1
- package/dist/astro/routes/api/import/wordpress/rewrite-url-helpers.mjs.map +1 -1
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts.map +1 -1
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +7 -7
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs.map +1 -1
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +4 -4
- package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +5 -5
- package/dist/astro/routes/api/manifest.mjs +3 -3
- package/dist/astro/routes/api/mcp.mjs +26 -26
- package/dist/astro/routes/api/media/_id_/confirm.mjs +4 -4
- package/dist/astro/routes/api/media/_id_.mjs +4 -4
- package/dist/astro/routes/api/media/file/_...key_.mjs +2 -2
- package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +3 -3
- package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +3 -3
- package/dist/astro/routes/api/media/providers/index.mjs +3 -3
- package/dist/astro/routes/api/media/upload-url.mjs +4 -4
- package/dist/astro/routes/api/media.mjs +5 -5
- package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +5 -5
- package/dist/astro/routes/api/menus/_name_/items.mjs +5 -5
- package/dist/astro/routes/api/menus/_name_/reorder.mjs +5 -5
- package/dist/astro/routes/api/menus/_name_/translations.mjs +5 -5
- package/dist/astro/routes/api/menus/_name_.mjs +5 -5
- package/dist/astro/routes/api/menus/index.mjs +5 -5
- package/dist/astro/routes/api/oauth/device/authorize.mjs +3 -3
- package/dist/astro/routes/api/oauth/device/code.mjs +4 -4
- package/dist/astro/routes/api/oauth/device/token.mjs +4 -4
- package/dist/astro/routes/api/oauth/register.mjs +2 -2
- package/dist/astro/routes/api/oauth/token/refresh.mjs +3 -3
- package/dist/astro/routes/api/oauth/token/revoke.mjs +3 -3
- package/dist/astro/routes/api/oauth/token.mjs +2 -2
- package/dist/astro/routes/api/openapi.json.mjs +2 -2
- package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +3 -3
- package/dist/astro/routes/api/redirects/404s/index.mjs +6 -6
- package/dist/astro/routes/api/redirects/404s/summary.mjs +6 -6
- package/dist/astro/routes/api/redirects/_id_.mjs +7 -7
- package/dist/astro/routes/api/redirects/index.mjs +7 -7
- package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +3 -3
- package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +3 -3
- package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +27 -27
- package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +27 -27
- package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +27 -27
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +27 -27
- package/dist/astro/routes/api/schema/collections/index.mjs +27 -27
- package/dist/astro/routes/api/schema/index.mjs +6 -6
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs +27 -27
- package/dist/astro/routes/api/schema/orphans/index.mjs +27 -27
- package/dist/astro/routes/api/search/enable.mjs +7 -7
- package/dist/astro/routes/api/search/index.mjs +6 -6
- package/dist/astro/routes/api/search/rebuild.mjs +7 -7
- package/dist/astro/routes/api/search/stats.mjs +6 -6
- package/dist/astro/routes/api/search/suggest.mjs +6 -6
- package/dist/astro/routes/api/sections/_slug_.mjs +6 -6
- package/dist/astro/routes/api/sections/index.mjs +6 -6
- package/dist/astro/routes/api/settings/email.mjs +4 -4
- package/dist/astro/routes/api/settings.mjs +8 -8
- package/dist/astro/routes/api/setup/admin-verify.mjs +3 -3
- package/dist/astro/routes/api/setup/admin.mjs +3 -3
- package/dist/astro/routes/api/setup/dev-bypass.mjs +15 -15
- package/dist/astro/routes/api/setup/dev-reset.mjs +2 -2
- package/dist/astro/routes/api/setup/index.mjs +16 -16
- package/dist/astro/routes/api/setup/status.mjs +3 -3
- package/dist/astro/routes/api/snapshot.mjs +4 -4
- package/dist/astro/routes/api/snapshot.mjs.map +1 -1
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +9 -9
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +9 -9
- package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +9 -9
- package/dist/astro/routes/api/taxonomies/index.mjs +9 -9
- package/dist/astro/routes/api/themes/preview.mjs +3 -3
- package/dist/astro/routes/api/typegen.mjs +5 -5
- package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +4 -4
- package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +6 -6
- package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +6 -6
- package/dist/astro/routes/api/widget-areas/_name_.mjs +5 -5
- package/dist/astro/routes/api/widget-areas/index.mjs +6 -6
- package/dist/astro/routes/api/widget-components.mjs +2 -2
- package/dist/astro/routes/robots.txt.mjs +4 -4
- package/dist/astro/routes/sitemap-_collection_.xml.d.mts.map +1 -1
- package/dist/astro/routes/sitemap-_collection_.xml.mjs +58 -13
- package/dist/astro/routes/sitemap-_collection_.xml.mjs.map +1 -1
- package/dist/astro/routes/sitemap.xml.mjs +5 -5
- package/dist/astro/types.d.mts +10 -3
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/{authorize-Bkwe8kuL.mjs → authorize-Bn4S4DUT.mjs} +2 -2
- package/dist/{authorize-Bkwe8kuL.mjs.map → authorize-Bn4S4DUT.mjs.map} +1 -1
- package/dist/{byline-CTaWkMh5.mjs → byline-BDylH_m4.mjs} +3 -3
- package/dist/{byline-CTaWkMh5.mjs.map → byline-BDylH_m4.mjs.map} +1 -1
- package/dist/{bylines-H0Xh5TMy.mjs → bylines-B7TFEvFf.mjs} +2 -2
- package/dist/{bylines-H0Xh5TMy.mjs.map → bylines-B7TFEvFf.mjs.map} +1 -1
- package/dist/{bylines-DtDRNF1n.d.mts → bylines-DWLnr6-k.d.mts} +17 -17
- package/dist/{bylines-DtDRNF1n.d.mts.map → bylines-DWLnr6-k.d.mts.map} +1 -1
- package/dist/{bylines-BYHWU3T7.mjs → bylines-n6nykUyI.mjs} +6 -6
- package/dist/{bylines-BYHWU3T7.mjs.map → bylines-n6nykUyI.mjs.map} +1 -1
- package/dist/{cache-CNk1jIxp.mjs → cache-BcI1yUjR.mjs} +2 -2
- package/dist/{cache-CNk1jIxp.mjs.map → cache-BcI1yUjR.mjs.map} +1 -1
- package/dist/{chunks-BkfVdD-3.mjs → chunks-cYG4SnIP.mjs} +2 -2
- package/dist/{chunks-BkfVdD-3.mjs.map → chunks-cYG4SnIP.mjs.map} +1 -1
- package/dist/cli/index.mjs +61 -15
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/cf-access.d.mts +1 -1
- package/dist/client/index.d.mts +1 -1
- package/dist/{comment-_yzlBYPx.mjs → comment-C76G-9tz.mjs} +2 -2
- package/dist/{comment-_yzlBYPx.mjs.map → comment-C76G-9tz.mjs.map} +1 -1
- package/dist/{comments-DxID-rsd.mjs → comments-CCxFFGY1.mjs} +3 -3
- package/dist/{comments-DxID-rsd.mjs.map → comments-CCxFFGY1.mjs.map} +1 -1
- package/dist/{content-C0ooIs-f.mjs → content-8voQNTXX.mjs} +3 -3
- package/dist/{content-C0ooIs-f.mjs.map → content-8voQNTXX.mjs.map} +1 -1
- package/dist/{context-sAnCaUIR.mjs → context-B7qiYrz2.mjs} +7 -7
- package/dist/{context-sAnCaUIR.mjs.map → context-B7qiYrz2.mjs.map} +1 -1
- package/dist/{dashboard-Cqw3ay2X.mjs → dashboard-BeaFSPpx.mjs} +4 -4
- package/dist/{dashboard-Cqw3ay2X.mjs.map → dashboard-BeaFSPpx.mjs.map} +1 -1
- package/dist/db/index.d.mts +1 -1
- package/dist/db/index.mjs +1 -1
- package/dist/db/sqlite.mjs +1 -1
- package/dist/{db-errors-CGN9kJfo.mjs → db-errors-BiYqoX-n.mjs} +14 -2
- package/dist/db-errors-BiYqoX-n.mjs.map +1 -0
- package/dist/{error-CPh_8eLq.mjs → error-ChfADBuu.mjs} +5 -3
- package/dist/error-ChfADBuu.mjs.map +1 -0
- package/dist/errors-9P_FDrJ_.mjs +17 -0
- package/dist/errors-9P_FDrJ_.mjs.map +1 -0
- package/dist/{fts-manager-Mnrtn-r2.mjs → fts-manager-C_b-4x8u.mjs} +2 -2
- package/dist/{fts-manager-Mnrtn-r2.mjs.map → fts-manager-C_b-4x8u.mjs.map} +1 -1
- package/dist/{index-Bv1Wf1zB.d.mts → index-D_p_jIP1.d.mts} +153 -109
- package/dist/index-D_p_jIP1.d.mts.map +1 -0
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +38 -38
- package/dist/{load-DmXNVhst.mjs → load-CLFRjk9r.mjs} +2 -2
- package/dist/{load-DmXNVhst.mjs.map → load-CLFRjk9r.mjs.map} +1 -1
- package/dist/{loader-Chm5h7Gr.mjs → loader-D-vIJjfY.mjs} +86 -46
- package/dist/loader-D-vIJjfY.mjs.map +1 -0
- package/dist/media/local-runtime.d.mts +3 -3
- package/dist/media/local-runtime.mjs +4 -4
- package/dist/{media-oqRcNiQf.mjs → media-CKQd8AYU.mjs} +2 -2
- package/dist/{media-oqRcNiQf.mjs.map → media-CKQd8AYU.mjs.map} +1 -1
- package/dist/{menus-C75SSmRy.mjs → menus-C-nWT5Tu.mjs} +17 -11
- package/dist/menus-C-nWT5Tu.mjs.map +1 -0
- package/dist/{menus-Bjf5R1Qq.mjs → menus-arUNspyU.mjs} +2 -2
- package/dist/{menus-Bjf5R1Qq.mjs.map → menus-arUNspyU.mjs.map} +1 -1
- package/dist/{parse-3-caTKgt.mjs → parse-DHbXfvxO.mjs} +2 -2
- package/dist/{parse-3-caTKgt.mjs.map → parse-DHbXfvxO.mjs.map} +1 -1
- package/dist/plugin-utils.d.mts +25 -10
- package/dist/plugin-utils.d.mts.map +1 -1
- package/dist/plugin-utils.mjs +11 -10
- package/dist/plugin-utils.mjs.map +1 -1
- package/dist/plugins/adapt-sandbox-entry.d.mts +3 -3
- package/dist/{query-BJn8TOPk.mjs → query-7m6-l0f_.mjs} +21 -14
- package/dist/query-7m6-l0f_.mjs.map +1 -0
- package/dist/{rate-limit-D_-gAeJ0.mjs → rate-limit-D8RAXN8b.mjs} +2 -2
- package/dist/{rate-limit-D_-gAeJ0.mjs.map → rate-limit-D8RAXN8b.mjs.map} +1 -1
- package/dist/{redirect-CNv4mHX2.mjs → redirect-CjfDGrTd.mjs} +2 -2
- package/dist/{redirect-CNv4mHX2.mjs.map → redirect-CjfDGrTd.mjs.map} +1 -1
- package/dist/{redirects-B-CUZ1Xh.mjs → redirects-CowoEHdE.mjs} +3 -3
- package/dist/{redirects-B-CUZ1Xh.mjs.map → redirects-CowoEHdE.mjs.map} +1 -1
- package/dist/{registry-DqrAQDXH.mjs → registry-Cyp-dx6J.mjs} +4 -4
- package/dist/{registry-DqrAQDXH.mjs.map → registry-Cyp-dx6J.mjs.map} +1 -1
- package/dist/resolve-D6sM-SgF.mjs +143 -0
- package/dist/resolve-D6sM-SgF.mjs.map +1 -0
- package/dist/{runner-CNHRo1mT.d.mts → runner-DSQBurMS.d.mts} +7 -4
- package/dist/runner-DSQBurMS.d.mts.map +1 -0
- package/dist/{runner-CGlojznK.mjs → runner-Drnvs96u.mjs} +20 -24
- package/dist/{runner-CGlojznK.mjs.map → runner-Drnvs96u.mjs.map} +1 -1
- package/dist/runtime.d.mts +3 -3
- package/dist/runtime.mjs +2 -2
- package/dist/{schema-Djdlfi5G.mjs → schema-CI9mYPX3.mjs} +4 -4
- package/dist/{schema-Djdlfi5G.mjs.map → schema-CI9mYPX3.mjs.map} +1 -1
- package/dist/{search-By-NN3da.mjs → search-DKz_mGBP.mjs} +4 -4
- package/dist/{search-By-NN3da.mjs.map → search-DKz_mGBP.mjs.map} +1 -1
- package/dist/{sections-DcBIlOq1.mjs → sections-DBbCDIAT.mjs} +3 -3
- package/dist/{sections-DcBIlOq1.mjs.map → sections-DBbCDIAT.mjs.map} +1 -1
- package/dist/seed/index.mjs +13 -13
- package/dist/{seo-bjDoq9Eg.mjs → seo-BGCyDlkb.mjs} +2 -2
- package/dist/{seo-bjDoq9Eg.mjs.map → seo-BGCyDlkb.mjs.map} +1 -1
- package/dist/{seo-BoR4wCUh.mjs → seo-Dq707mNQ.mjs} +5 -3
- package/dist/seo-Dq707mNQ.mjs.map +1 -0
- package/dist/{service-BuuTdGAT.mjs → service-B0H7U1Y9.mjs} +2 -2
- package/dist/{service-BuuTdGAT.mjs.map → service-B0H7U1Y9.mjs.map} +1 -1
- package/dist/{settings-hcubRfkr.mjs → settings-BSXRtTzk.mjs} +3 -3
- package/dist/{settings-hcubRfkr.mjs.map → settings-BSXRtTzk.mjs.map} +1 -1
- package/dist/{settings-CJnKiWuR.mjs → settings-DfwNyQkf.mjs} +3 -3
- package/dist/{settings-CJnKiWuR.mjs.map → settings-DfwNyQkf.mjs.map} +1 -1
- package/dist/{taxonomies-CLs9HPE2.mjs → taxonomies-4vx0nmMr.mjs} +4 -4
- package/dist/{taxonomies-CLs9HPE2.mjs.map → taxonomies-4vx0nmMr.mjs.map} +1 -1
- package/dist/{taxonomies-WamPVA2x.mjs → taxonomies-CcvrMLbR.mjs} +7 -7
- package/dist/{taxonomies-WamPVA2x.mjs.map → taxonomies-CcvrMLbR.mjs.map} +1 -1
- package/dist/{taxonomy-D4Uc2LsZ.mjs → taxonomy-zqGQUqgu.mjs} +3 -3
- package/dist/{taxonomy-D4Uc2LsZ.mjs.map → taxonomy-zqGQUqgu.mjs.map} +1 -1
- package/dist/{transport-DOxLfUir.d.mts → transport-C2MGqtL6.d.mts} +1 -1
- package/dist/{transport-DOxLfUir.d.mts.map → transport-C2MGqtL6.d.mts.map} +1 -1
- package/dist/{types-ByV5sgsv.mjs → types-B0bmgwMG.mjs} +2 -2
- package/dist/{types-ByV5sgsv.mjs.map → types-B0bmgwMG.mjs.map} +1 -1
- package/dist/{user-D3BD5zdT.mjs → user-hUSOaIJy.mjs} +2 -2
- package/dist/{user-D3BD5zdT.mjs.map → user-hUSOaIJy.mjs.map} +1 -1
- package/dist/{validate-mz87i8_1.mjs → validate-IGltez8n.mjs} +2 -2
- package/dist/{validate-mz87i8_1.mjs.map → validate-IGltez8n.mjs.map} +1 -1
- package/dist/{validation-DKHhXjPr.mjs → validation-Bmymau7y.mjs} +6 -6
- package/dist/{validation-DKHhXjPr.mjs.map → validation-Bmymau7y.mjs.map} +1 -1
- package/dist/version-ITD3PlQd.mjs +7 -0
- package/dist/{version-Ct7C6RSo.mjs.map → version-ITD3PlQd.mjs.map} +1 -1
- package/dist/{widgets-lShIQXU5.mjs → widgets-yHQa4c6c.mjs} +2 -2
- package/dist/{widgets-lShIQXU5.mjs.map → widgets-yHQa4c6c.mjs.map} +1 -1
- package/dist/{zod-generator-dvxgmd1M.mjs → zod-generator-B80aap1J.mjs} +2 -2
- package/dist/{zod-generator-dvxgmd1M.mjs.map → zod-generator-B80aap1J.mjs.map} +1 -1
- package/package.json +7 -7
- package/src/api/errors.ts +2 -0
- package/src/api/handlers/index.ts +2 -0
- package/src/api/handlers/registry.ts +69 -1
- package/src/api/handlers/seo.ts +16 -1
- package/src/api/handlers/snapshot.ts +1 -1
- package/src/astro/integration/index.ts +26 -0
- package/src/astro/integration/routes.ts +5 -0
- package/src/astro/integration/runtime.ts +8 -0
- package/src/astro/middleware.ts +4 -0
- package/src/astro/public-plugin-api-routes.ts +41 -0
- package/src/astro/routes/api/admin/plugins/registry/[id]/update.ts +4 -0
- package/src/astro/routes/api/admin/plugins/registry/artifact.ts +388 -0
- package/src/astro/routes/api/admin/plugins/registry/install.ts +7 -1
- package/src/astro/routes/api/import/wordpress/rewrite-url-helpers.ts +22 -0
- package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +5 -2
- package/src/astro/routes/sitemap-[collection].xml.ts +114 -14
- package/src/astro/types.ts +14 -0
- package/src/content/converters/portable-text-to-prosemirror.ts +35 -11
- package/src/database/connection.ts +3 -10
- package/src/database/errors.ts +14 -0
- package/src/database/index.ts +3 -1
- package/src/database/migrations/runner.ts +29 -21
- package/src/emdash-runtime.ts +1 -0
- package/src/i18n/resolve.ts +152 -0
- package/src/index.ts +2 -0
- package/src/loader.ts +133 -59
- package/src/plugin-utils.ts +23 -0
- package/src/query.ts +24 -5
- package/src/utils/db-errors.ts +24 -0
- package/dist/connection-2igzM-AT.mjs +0 -57
- package/dist/connection-2igzM-AT.mjs.map +0 -1
- package/dist/db-errors-CGN9kJfo.mjs.map +0 -1
- package/dist/error-CPh_8eLq.mjs.map +0 -1
- package/dist/index-Bv1Wf1zB.d.mts.map +0 -1
- package/dist/loader-Chm5h7Gr.mjs.map +0 -1
- package/dist/menus-C75SSmRy.mjs.map +0 -1
- package/dist/query-BJn8TOPk.mjs.map +0 -1
- package/dist/resolve-Cj98DuqN.mjs +0 -39
- package/dist/resolve-Cj98DuqN.mjs.map +0 -1
- package/dist/runner-CNHRo1mT.d.mts.map +0 -1
- package/dist/seo-BoR4wCUh.mjs.map +0 -1
- package/dist/version-Ct7C6RSo.mjs +0 -7
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry artifact proxy
|
|
3
|
+
*
|
|
4
|
+
* GET /_emdash/api/admin/plugins/registry/artifact?did=&slug=&version=&kind=&index=
|
|
5
|
+
*
|
|
6
|
+
* Proxies an icon / screenshot / banner image referenced by a registry
|
|
7
|
+
* release record so the admin UI can display it without cross-origin
|
|
8
|
+
* requests to arbitrary publisher hosting.
|
|
9
|
+
*
|
|
10
|
+
* Trust model (CRITICAL): the proxy never accepts an artifact URL from the
|
|
11
|
+
* client. The caller addresses an artifact by its coordinates
|
|
12
|
+
* `(did, slug, version, kind, index)`; the server resolves the *declared*
|
|
13
|
+
* URL from the validated release record fetched from the configured
|
|
14
|
+
* aggregator. The proxy can therefore only ever fetch a URL the publisher
|
|
15
|
+
* declared in their signed release — not an arbitrary caller-supplied URL.
|
|
16
|
+
*
|
|
17
|
+
* The publisher-declared URL is still untrusted (an attacker who controls a
|
|
18
|
+
* publisher record, or the aggregator, can point it anywhere), so the
|
|
19
|
+
* resolved URL passes through the SSRF defences (`assertSafeArtifactUrl`,
|
|
20
|
+
* re-validated on every redirect hop) before any fetch, and only allowlisted
|
|
21
|
+
* image content types are served back.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { Did } from "@atcute/lexicons";
|
|
25
|
+
import type { APIRoute } from "astro";
|
|
26
|
+
|
|
27
|
+
import { requirePerm } from "#api/authorize.js";
|
|
28
|
+
import { apiError } from "#api/error.js";
|
|
29
|
+
import { assertSafeArtifactUrl } from "#api/index.js";
|
|
30
|
+
|
|
31
|
+
import { coerceRegistryConfig, validateAggregatorUrl } from "../../../../../../registry/config.js";
|
|
32
|
+
|
|
33
|
+
export const prerender = false;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Image content types the proxy will pass through. Anything else is rejected.
|
|
37
|
+
*
|
|
38
|
+
* SVG is deliberately excluded: it is active content (an `<svg><script>`
|
|
39
|
+
* executes when navigated to as a top-level document), and the publisher
|
|
40
|
+
* supplies the bytes. Rather than serve it behind mitigations, we refuse it
|
|
41
|
+
* end-to-end — the publish CLI rejects SVG artifacts too, so a conforming
|
|
42
|
+
* release never references one. AVIF is included.
|
|
43
|
+
*/
|
|
44
|
+
const ALLOWED_IMAGE_TYPES = new Set([
|
|
45
|
+
"image/png",
|
|
46
|
+
"image/jpeg",
|
|
47
|
+
"image/webp",
|
|
48
|
+
"image/gif",
|
|
49
|
+
"image/avif",
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
/** Artifact kinds the proxy can resolve. `screenshot` additionally needs `index`. */
|
|
53
|
+
const ALLOWED_KINDS = new Set(["icon", "banner", "screenshot"]);
|
|
54
|
+
|
|
55
|
+
/** Loose DID shape (`did:method:id`); the aggregator lexicon is authoritative. */
|
|
56
|
+
const DID_PATTERN = /^did:[a-z]+:.+/;
|
|
57
|
+
/** Slug grammar: ASCII letter then letters / digits / `-` / `_`. Mirrors the install route. */
|
|
58
|
+
const SLUG_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
|
|
59
|
+
/** Non-negative integer, for the screenshot index param. */
|
|
60
|
+
const INDEX_PATTERN = /^\d+$/;
|
|
61
|
+
|
|
62
|
+
/** Cap proxied images so a hostile host can't stream an unbounded body. */
|
|
63
|
+
const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
64
|
+
|
|
65
|
+
/** Redirect hops to follow, re-validating each target against SSRF rules. */
|
|
66
|
+
const MAX_REDIRECTS = 5;
|
|
67
|
+
|
|
68
|
+
/** Wall-clock budget covering connect + headers + body for the artifact fetch. */
|
|
69
|
+
const FETCH_TIMEOUT_MS = 15_000;
|
|
70
|
+
|
|
71
|
+
/** Per-aggregator-request timeout and overall budget for release resolution. */
|
|
72
|
+
const AGGREGATOR_REQUEST_TIMEOUT_MS = 15_000;
|
|
73
|
+
const AGGREGATOR_TOTAL_BUDGET_MS = 30_000;
|
|
74
|
+
|
|
75
|
+
/** Bound the version search: 20 pages * 50 per page = 1000 releases worth. */
|
|
76
|
+
const MAX_LIST_PAGES = 20;
|
|
77
|
+
|
|
78
|
+
/** Build a fetch that enforces a per-request and per-budget timeout. Mirrors the install handler. */
|
|
79
|
+
function timedFetch(totalDeadline: number): typeof fetch {
|
|
80
|
+
return (input: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => {
|
|
81
|
+
const now = Date.now();
|
|
82
|
+
const remaining = Math.max(0, totalDeadline - now);
|
|
83
|
+
if (remaining === 0) {
|
|
84
|
+
return Promise.reject(new Error("Aggregator request budget exhausted"));
|
|
85
|
+
}
|
|
86
|
+
const timeout = Math.min(AGGREGATOR_REQUEST_TIMEOUT_MS, remaining);
|
|
87
|
+
const controller = new AbortController();
|
|
88
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
89
|
+
const callerSignal = init?.signal;
|
|
90
|
+
if (callerSignal) {
|
|
91
|
+
if (callerSignal.aborted) controller.abort(callerSignal.reason);
|
|
92
|
+
else callerSignal.addEventListener("abort", () => controller.abort(callerSignal.reason));
|
|
93
|
+
}
|
|
94
|
+
return fetch(input, { ...init, signal: controller.signal }).finally(() => {
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Narrow one entry of a release's `artifacts` map to a usable image URL.
|
|
102
|
+
*
|
|
103
|
+
* The embedded `release` record is lexicon-validated at the DiscoveryClient
|
|
104
|
+
* boundary, but `artifacts` is an aggregator pass-through typed `unknown`, so
|
|
105
|
+
* the entry's shape is not guaranteed. Returns the `url` string only when the
|
|
106
|
+
* value is an object carrying a non-empty string `url`; everything else
|
|
107
|
+
* (missing key, wrong type, no `url`) yields `null`.
|
|
108
|
+
*/
|
|
109
|
+
function declaredArtifactUrl(value: unknown): string | null {
|
|
110
|
+
if (!value || typeof value !== "object") return null;
|
|
111
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- narrowed to non-null object above; url checked below
|
|
112
|
+
const entry = value as Record<string, unknown>;
|
|
113
|
+
const url = entry.url;
|
|
114
|
+
if (typeof url !== "string" || url.length === 0) return null;
|
|
115
|
+
return url;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Resolve the declared artifact URL for `(kind, index)` from a release's
|
|
120
|
+
* `artifacts` map. Returns `null` when the requested artifact isn't present
|
|
121
|
+
* or doesn't carry a usable URL.
|
|
122
|
+
*/
|
|
123
|
+
function resolveDeclaredUrl(artifacts: unknown, kind: string, index: number): string | null {
|
|
124
|
+
if (!artifacts || typeof artifacts !== "object") return null;
|
|
125
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- narrowed to non-null object above; each entry shape-narrowed by declaredArtifactUrl
|
|
126
|
+
const map = artifacts as Record<string, unknown>;
|
|
127
|
+
|
|
128
|
+
if (kind === "icon") return declaredArtifactUrl(map.icon);
|
|
129
|
+
if (kind === "banner") return declaredArtifactUrl(map.banner);
|
|
130
|
+
// kind === "screenshot"
|
|
131
|
+
const screenshots = map.screenshots;
|
|
132
|
+
if (!Array.isArray(screenshots)) return null;
|
|
133
|
+
if (index < 0 || index >= screenshots.length) return null;
|
|
134
|
+
return declaredArtifactUrl(screenshots[index]);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const GET: APIRoute = async ({ url, locals }) => {
|
|
138
|
+
const { emdash, user } = locals;
|
|
139
|
+
|
|
140
|
+
if (!emdash?.db) {
|
|
141
|
+
return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const denied = requirePerm(user, "plugins:read");
|
|
145
|
+
if (denied) return denied;
|
|
146
|
+
|
|
147
|
+
const did = url.searchParams.get("did");
|
|
148
|
+
const slug = url.searchParams.get("slug");
|
|
149
|
+
const kind = url.searchParams.get("kind");
|
|
150
|
+
const versionParam = url.searchParams.get("version");
|
|
151
|
+
const indexParam = url.searchParams.get("index");
|
|
152
|
+
|
|
153
|
+
if (!did || !slug || !kind) {
|
|
154
|
+
return apiError("INVALID_REQUEST", "Missing did, slug, or kind", 400);
|
|
155
|
+
}
|
|
156
|
+
if (did.length > 256 || !DID_PATTERN.test(did)) {
|
|
157
|
+
return apiError("INVALID_REQUEST", "Invalid did", 400);
|
|
158
|
+
}
|
|
159
|
+
if (slug.length > 64 || !SLUG_PATTERN.test(slug)) {
|
|
160
|
+
return apiError("INVALID_REQUEST", "Invalid slug", 400);
|
|
161
|
+
}
|
|
162
|
+
if (!ALLOWED_KINDS.has(kind)) {
|
|
163
|
+
return apiError("INVALID_REQUEST", "Invalid kind", 400);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let index = 0;
|
|
167
|
+
if (kind === "screenshot") {
|
|
168
|
+
if (indexParam === null) {
|
|
169
|
+
return apiError("INVALID_REQUEST", "Missing index for screenshot", 400);
|
|
170
|
+
}
|
|
171
|
+
if (!INDEX_PATTERN.test(indexParam)) {
|
|
172
|
+
return apiError("INVALID_REQUEST", "Invalid index", 400);
|
|
173
|
+
}
|
|
174
|
+
index = Number(indexParam);
|
|
175
|
+
if (!Number.isSafeInteger(index)) {
|
|
176
|
+
return apiError("INVALID_REQUEST", "Invalid index", 400);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let version: string | undefined;
|
|
181
|
+
if (versionParam !== null && versionParam.length > 0) {
|
|
182
|
+
if (versionParam.length > 64) {
|
|
183
|
+
return apiError("INVALID_REQUEST", "Invalid version", 400);
|
|
184
|
+
}
|
|
185
|
+
version = versionParam;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const registryConfig = coerceRegistryConfig(emdash.config.experimental?.registry);
|
|
189
|
+
if (!registryConfig) {
|
|
190
|
+
return apiError("REGISTRY_NOT_CONFIGURED", "Registry is not configured", 400);
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
validateAggregatorUrl(registryConfig.aggregatorUrl);
|
|
194
|
+
} catch {
|
|
195
|
+
return apiError("REGISTRY_NOT_CONFIGURED", "Registry aggregator URL is invalid", 500);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Resolve the publisher-declared artifact URL from the release record.
|
|
199
|
+
let declaredUrl: string;
|
|
200
|
+
try {
|
|
201
|
+
const resolved = await resolveArtifactUrl(registryConfig, did, slug, version, kind, index);
|
|
202
|
+
if (resolved === null) {
|
|
203
|
+
return apiError("ARTIFACT_NOT_FOUND", "Artifact not found", 404);
|
|
204
|
+
}
|
|
205
|
+
declaredUrl = resolved;
|
|
206
|
+
} catch {
|
|
207
|
+
return apiError("ARTIFACT_RESOLVE_FAILED", "Failed to resolve artifact", 502);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const controller = new AbortController();
|
|
211
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
212
|
+
try {
|
|
213
|
+
// `assertSafeArtifactUrl` validates scheme / credentials / loopback +
|
|
214
|
+
// resolves the hostname and rejects private / link-local / metadata
|
|
215
|
+
// targets (DNS-rebinding defence). It throws a plain Error on any
|
|
216
|
+
// block, so a rejection here means the URL is unsafe.
|
|
217
|
+
let current: URL;
|
|
218
|
+
try {
|
|
219
|
+
current = await assertSafeArtifactUrl(declaredUrl);
|
|
220
|
+
} catch {
|
|
221
|
+
return apiError("ARTIFACT_URL_REJECTED", "Artifact URL is not allowed", 400);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let response: Response;
|
|
225
|
+
for (let hop = 0; ; hop++) {
|
|
226
|
+
response = await fetch(current.href, { redirect: "manual", signal: controller.signal });
|
|
227
|
+
if (response.status < 300 || response.status >= 400) break;
|
|
228
|
+
const location = response.headers.get("location");
|
|
229
|
+
if (!location) break;
|
|
230
|
+
if (hop === MAX_REDIRECTS) {
|
|
231
|
+
return apiError("ARTIFACT_URL_REJECTED", "Too many redirects", 502);
|
|
232
|
+
}
|
|
233
|
+
let next: URL;
|
|
234
|
+
try {
|
|
235
|
+
next = await assertSafeArtifactUrl(new URL(location, current).href);
|
|
236
|
+
} catch {
|
|
237
|
+
return apiError("ARTIFACT_URL_REJECTED", "Redirect target is not allowed", 400);
|
|
238
|
+
}
|
|
239
|
+
current = next;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!response.ok) {
|
|
243
|
+
return apiError("ARTIFACT_FETCH_FAILED", "Failed to fetch artifact", 502);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Content-Type allowlist: only image types are proxied. A non-image
|
|
247
|
+
// (HTML error page, JSON, octet-stream) is rejected so the admin
|
|
248
|
+
// never renders publisher-controlled markup from the EmDash origin.
|
|
249
|
+
const rawType = response.headers.get("content-type") ?? "";
|
|
250
|
+
const contentType = rawType.split(";", 1)[0]!.trim().toLowerCase();
|
|
251
|
+
if (!ALLOWED_IMAGE_TYPES.has(contentType)) {
|
|
252
|
+
return apiError("ARTIFACT_NOT_IMAGE", "Artifact is not an allowed image type", 415);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const declaredLength = response.headers.get("content-length");
|
|
256
|
+
if (declaredLength) {
|
|
257
|
+
const declared = Number(declaredLength);
|
|
258
|
+
if (Number.isFinite(declared) && declared > MAX_IMAGE_BYTES) {
|
|
259
|
+
return apiError("ARTIFACT_TOO_LARGE", "Artifact exceeds size limit", 413);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const bytes = await readCapped(response, MAX_IMAGE_BYTES);
|
|
264
|
+
if (bytes === null) {
|
|
265
|
+
return apiError("ARTIFACT_TOO_LARGE", "Artifact exceeds size limit", 413);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Only the allowlisted Content-Type is forwarded — never copy other
|
|
269
|
+
// upstream headers. `private, no-store` keeps publisher images out of
|
|
270
|
+
// shared caches in the authenticated admin origin.
|
|
271
|
+
//
|
|
272
|
+
// SVG is not in the allowlist, so active-content bytes never reach
|
|
273
|
+
// here. `Content-Disposition: attachment`, the sandbox CSP, and
|
|
274
|
+
// `nosniff` remain as defence-in-depth: they force a download and
|
|
275
|
+
// neutralise script/plugins for any image type if a client navigates
|
|
276
|
+
// directly to the proxy URL.
|
|
277
|
+
return new Response(bytes, {
|
|
278
|
+
headers: {
|
|
279
|
+
"Content-Type": contentType,
|
|
280
|
+
"Cache-Control": "private, no-store",
|
|
281
|
+
"X-Content-Type-Options": "nosniff",
|
|
282
|
+
"Content-Disposition": "attachment",
|
|
283
|
+
"Content-Security-Policy": "default-src 'none'; sandbox",
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
} catch {
|
|
287
|
+
return apiError("ARTIFACT_FETCH_FAILED", "Failed to fetch artifact", 502);
|
|
288
|
+
} finally {
|
|
289
|
+
clearTimeout(timer);
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Resolve the declared artifact URL for `(did, slug, version, kind, index)`
|
|
295
|
+
* from the aggregator's release record. Mirrors the install handler's release
|
|
296
|
+
* lookup. Returns `null` when the package/release/artifact isn't found.
|
|
297
|
+
*
|
|
298
|
+
* Self-contained to this route: the install/update handlers are intentionally
|
|
299
|
+
* left untouched, so a small amount of resolution-pattern duplication is
|
|
300
|
+
* accepted here.
|
|
301
|
+
*/
|
|
302
|
+
async function resolveArtifactUrl(
|
|
303
|
+
registryConfig: { aggregatorUrl: string; acceptLabelers?: string },
|
|
304
|
+
did: string,
|
|
305
|
+
slug: string,
|
|
306
|
+
version: string | undefined,
|
|
307
|
+
kind: string,
|
|
308
|
+
index: number,
|
|
309
|
+
): Promise<string | null> {
|
|
310
|
+
// Lazy-load the discovery client so the `@atcute/client` dependency only
|
|
311
|
+
// loads when the registry path is exercised.
|
|
312
|
+
const { DiscoveryClient } = await import("@emdash-cms/registry-client/discovery");
|
|
313
|
+
|
|
314
|
+
const aggregatorDeadline = Date.now() + AGGREGATOR_TOTAL_BUDGET_MS;
|
|
315
|
+
const discovery = new DiscoveryClient({
|
|
316
|
+
aggregatorUrl: registryConfig.aggregatorUrl,
|
|
317
|
+
acceptLabelers: registryConfig.acceptLabelers,
|
|
318
|
+
fetch: timedFetch(aggregatorDeadline),
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- DID shape validated by the route before this call
|
|
322
|
+
const publisherDid = did as Did;
|
|
323
|
+
|
|
324
|
+
const releaseView = await (async () => {
|
|
325
|
+
if (!version) {
|
|
326
|
+
return discovery.getLatestRelease({ did: publisherDid, package: slug });
|
|
327
|
+
}
|
|
328
|
+
let cursor: string | undefined;
|
|
329
|
+
const seenCursors = new Set<string>();
|
|
330
|
+
for (let page = 0; page < MAX_LIST_PAGES; page++) {
|
|
331
|
+
if (cursor !== undefined) {
|
|
332
|
+
if (seenCursors.has(cursor)) break;
|
|
333
|
+
seenCursors.add(cursor);
|
|
334
|
+
}
|
|
335
|
+
const result = await discovery.listReleases({
|
|
336
|
+
did: publisherDid,
|
|
337
|
+
package: slug,
|
|
338
|
+
cursor,
|
|
339
|
+
limit: 50,
|
|
340
|
+
});
|
|
341
|
+
for (const r of result.releases) {
|
|
342
|
+
if (r.version === version) return r;
|
|
343
|
+
}
|
|
344
|
+
if (!result.cursor) break;
|
|
345
|
+
cursor = result.cursor;
|
|
346
|
+
}
|
|
347
|
+
return undefined;
|
|
348
|
+
})();
|
|
349
|
+
|
|
350
|
+
if (!releaseView?.release) return null;
|
|
351
|
+
|
|
352
|
+
return resolveDeclaredUrl(releaseView.release.artifacts, kind, index);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Read a response body into memory, aborting once it exceeds `limit`. Returns
|
|
357
|
+
* `null` when the cap is breached (the streamed body lied about / omitted
|
|
358
|
+
* Content-Length). The cap is the real defence against an unbounded body.
|
|
359
|
+
*/
|
|
360
|
+
async function readCapped(response: Response, limit: number): Promise<Uint8Array | null> {
|
|
361
|
+
const body = response.body;
|
|
362
|
+
if (!body) {
|
|
363
|
+
const buf = new Uint8Array(await response.arrayBuffer());
|
|
364
|
+
return buf.length > limit ? null : buf;
|
|
365
|
+
}
|
|
366
|
+
const reader = body.getReader();
|
|
367
|
+
const chunks: Uint8Array[] = [];
|
|
368
|
+
let total = 0;
|
|
369
|
+
while (true) {
|
|
370
|
+
const { done, value } = await reader.read();
|
|
371
|
+
if (done) break;
|
|
372
|
+
if (value) {
|
|
373
|
+
total += value.length;
|
|
374
|
+
if (total > limit) {
|
|
375
|
+
await reader.cancel();
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
chunks.push(value);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
const combined = new Uint8Array(total);
|
|
382
|
+
let offset = 0;
|
|
383
|
+
for (const chunk of chunks) {
|
|
384
|
+
combined.set(chunk, offset);
|
|
385
|
+
offset += chunk.length;
|
|
386
|
+
}
|
|
387
|
+
return combined;
|
|
388
|
+
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* view time (handle is best-effort per the lexicon).
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { hostEnvFromVersions } from "@emdash-cms/registry-client/env";
|
|
15
16
|
import type { APIRoute } from "astro";
|
|
16
17
|
import { z } from "zod";
|
|
17
18
|
|
|
@@ -20,6 +21,8 @@ import { apiError, handleError, unwrapResult } from "#api/error.js";
|
|
|
20
21
|
import { handleRegistryInstall } from "#api/index.js";
|
|
21
22
|
import { isParseError, parseBody } from "#api/parse.js";
|
|
22
23
|
|
|
24
|
+
import { VERSION } from "../../../../../../version.js";
|
|
25
|
+
|
|
23
26
|
export const prerender = false;
|
|
24
27
|
|
|
25
28
|
const installBodySchema = z.object({
|
|
@@ -91,7 +94,10 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
91
94
|
version: body.version,
|
|
92
95
|
acknowledgedDeclaredAccess: body.acknowledgedDeclaredAccess,
|
|
93
96
|
},
|
|
94
|
-
{
|
|
97
|
+
{
|
|
98
|
+
configuredPluginIds: reservedPluginIds,
|
|
99
|
+
hostEnv: hostEnvFromVersions(VERSION, emdash.config.astroVersion),
|
|
100
|
+
},
|
|
95
101
|
);
|
|
96
102
|
|
|
97
103
|
if (!result.success) return unwrapResult(result);
|
|
@@ -27,6 +27,28 @@ export function buildBaseUrlMap(urlMap: Record<string, string>): Map<string, str
|
|
|
27
27
|
return baseMap;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Extract the URL to match from a stored media field value.
|
|
32
|
+
*
|
|
33
|
+
* Image/file columns hold a JSON-stringified MediaValue
|
|
34
|
+
* (e.g. `{"provider":"external","id":"","src":"https://.../hero.jpg"}`), but legacy
|
|
35
|
+
* rows may hold a bare URL string. Returns the inner `src` for a MediaValue, otherwise
|
|
36
|
+
* the value unchanged. Without this, the whole JSON blob is passed to findMatchingUrl()
|
|
37
|
+
* and the embedded URL is never matched.
|
|
38
|
+
*/
|
|
39
|
+
export function extractMediaUrl(value: string): string {
|
|
40
|
+
try {
|
|
41
|
+
// eslint-disable-next-line typescript/no-unsafe-type-assertion -- shape validated below
|
|
42
|
+
const parsed = JSON.parse(value) as { src?: unknown };
|
|
43
|
+
if (parsed && typeof parsed.src === "string") {
|
|
44
|
+
return parsed.src;
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
// Not JSON — treat the column value as a bare URL.
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
30
52
|
/**
|
|
31
53
|
* Find matching new URL for a given URL, checking exact, base, and WordPress image-size matches
|
|
32
54
|
*/
|
|
@@ -24,6 +24,7 @@ import type { EmDashHandlers } from "#types";
|
|
|
24
24
|
|
|
25
25
|
import {
|
|
26
26
|
buildBaseUrlMap,
|
|
27
|
+
extractMediaUrl,
|
|
27
28
|
findMatchingUrl,
|
|
28
29
|
rewritePortableTextUrls,
|
|
29
30
|
rewriteStringUrls,
|
|
@@ -174,8 +175,10 @@ async function rewriteUrls(
|
|
|
174
175
|
const value = row[field.slug];
|
|
175
176
|
if (!value || typeof value !== "string") continue;
|
|
176
177
|
|
|
177
|
-
//
|
|
178
|
-
|
|
178
|
+
// Values are stored as JSON MediaValue objects (e.g. featured_image from
|
|
179
|
+
// import normalizes to {"provider":"external","src":"<wp url>"}). Match on the
|
|
180
|
+
// inner `src`, falling back to the raw value for legacy bare-URL rows.
|
|
181
|
+
const newUrl = findMatchingUrl(extractMediaUrl(value), urlMap, baseMap);
|
|
179
182
|
if (newUrl) {
|
|
180
183
|
// Normalize into a proper MediaValue instead of storing a bare URL
|
|
181
184
|
try {
|
|
@@ -5,13 +5,25 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Uses the collection's url_pattern to build URLs. Falls back to
|
|
7
7
|
* /{collection}/{slug} when no pattern is configured.
|
|
8
|
+
*
|
|
9
|
+
* i18n behaviour: when Astro i18n is enabled, the locale prefix is
|
|
10
|
+
* applied via Astro's own `getRelativeLocaleUrl` (which honours
|
|
11
|
+
* `prefixDefaultLocale`, custom `path` mappings, and other `routing`
|
|
12
|
+
* config). Each translation row is emitted as its own `<url>` with
|
|
13
|
+
* `<xhtml:link rel="alternate" hreflang="...">` entries pointing to
|
|
14
|
+
* its siblings (grouped by `translation_group`). The default-locale
|
|
15
|
+
* variant is also linked as `hreflang="x-default"`.
|
|
8
16
|
*/
|
|
9
17
|
|
|
10
18
|
import type { APIRoute } from "astro";
|
|
11
19
|
|
|
12
20
|
import { handleSitemapData } from "#api/handlers/seo.js";
|
|
21
|
+
import { getPublicOrigin } from "#api/public-url.js";
|
|
13
22
|
import { getSiteSettingsWithDb } from "#settings/index.js";
|
|
14
23
|
|
|
24
|
+
import { getI18nConfig, isI18nEnabled } from "../../i18n/config.js";
|
|
25
|
+
import { interpolateUrlPattern, localizePath } from "../../i18n/resolve.js";
|
|
26
|
+
|
|
15
27
|
export const prerender = false;
|
|
16
28
|
|
|
17
29
|
const TRAILING_SLASH_RE = /\/$/;
|
|
@@ -20,8 +32,6 @@ const LT_RE = /</g;
|
|
|
20
32
|
const GT_RE = />/g;
|
|
21
33
|
const QUOT_RE = /"/g;
|
|
22
34
|
const APOS_RE = /'/g;
|
|
23
|
-
const SLUG_PLACEHOLDER = "{slug}";
|
|
24
|
-
const ID_PLACEHOLDER = "{id}";
|
|
25
35
|
|
|
26
36
|
export const GET: APIRoute = async ({ params, locals, url }) => {
|
|
27
37
|
const { emdash } = locals;
|
|
@@ -36,7 +46,10 @@ export const GET: APIRoute = async ({ params, locals, url }) => {
|
|
|
36
46
|
|
|
37
47
|
try {
|
|
38
48
|
const settings = await getSiteSettingsWithDb(emdash.db);
|
|
39
|
-
const siteUrl = (settings.url || url
|
|
49
|
+
const siteUrl = (settings.url || getPublicOrigin(url, emdash?.config)).replace(
|
|
50
|
+
TRAILING_SLASH_RE,
|
|
51
|
+
"",
|
|
52
|
+
);
|
|
40
53
|
|
|
41
54
|
const result = await handleSitemapData(emdash.db, collectionSlug);
|
|
42
55
|
|
|
@@ -55,25 +68,112 @@ export const GET: APIRoute = async ({ params, locals, url }) => {
|
|
|
55
68
|
});
|
|
56
69
|
}
|
|
57
70
|
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
|
|
61
|
-
];
|
|
71
|
+
const i18nEnabled = isI18nEnabled();
|
|
72
|
+
const i18nConfig = getI18nConfig();
|
|
62
73
|
|
|
74
|
+
// Group entries by `translation_group` so each <url> can advertise
|
|
75
|
+
// its sibling translations via xhtml:link. Rows without a group
|
|
76
|
+
// (legacy/single-locale data) are emitted individually.
|
|
77
|
+
type Entry = (typeof col.entries)[number];
|
|
78
|
+
const groups = new Map<string, Entry[]>();
|
|
79
|
+
const ungrouped: Entry[] = [];
|
|
63
80
|
for (const entry of col.entries) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
81
|
+
if (i18nEnabled && entry.translationGroup) {
|
|
82
|
+
const list = groups.get(entry.translationGroup);
|
|
83
|
+
if (list) list.push(entry);
|
|
84
|
+
else groups.set(entry.translationGroup, [entry]);
|
|
85
|
+
} else {
|
|
86
|
+
ungrouped.push(entry);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
70
89
|
|
|
71
|
-
|
|
90
|
+
// Resolve every URL up-front so we can reference sibling URLs
|
|
91
|
+
// while emitting hreflang alternates without re-resolving.
|
|
92
|
+
// `localizePath` returns `null` when the row's locale isn't in
|
|
93
|
+
// the configured `i18n.locales` list -- the site can't serve a
|
|
94
|
+
// route for it, so the entry is dropped from the sitemap and
|
|
95
|
+
// omitted from sibling alternates.
|
|
96
|
+
const urlByEntry = new Map<string, string | null>();
|
|
97
|
+
const resolveEntryUrl = async (entry: Entry): Promise<string | null> => {
|
|
98
|
+
if (urlByEntry.has(entry.id)) return urlByEntry.get(entry.id) ?? null;
|
|
99
|
+
const path = interpolateUrlPattern({
|
|
100
|
+
pattern: col.urlPattern,
|
|
101
|
+
collection: col.collection,
|
|
102
|
+
slug: entry.slug || entry.id,
|
|
103
|
+
id: entry.id,
|
|
104
|
+
});
|
|
105
|
+
const localized = await localizePath(path, entry.locale);
|
|
106
|
+
const absolute = localized === null ? null : `${siteUrl}${localized}`;
|
|
107
|
+
urlByEntry.set(entry.id, absolute);
|
|
108
|
+
return absolute;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const useXhtml = i18nEnabled;
|
|
112
|
+
const lines: string[] = ['<?xml version="1.0" encoding="UTF-8"?>'];
|
|
113
|
+
lines.push(
|
|
114
|
+
useXhtml
|
|
115
|
+
? '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">'
|
|
116
|
+
: '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const writeUrl = async (entry: Entry, siblings: Entry[] | null) => {
|
|
120
|
+
const loc = await resolveEntryUrl(entry);
|
|
121
|
+
// Skip rows whose locale isn't in the configured `i18n.locales`
|
|
122
|
+
// list. Linking to a route the site can't serve is worse than
|
|
123
|
+
// no link at all (search engines hit a 404 and downrank).
|
|
124
|
+
if (loc === null) return;
|
|
72
125
|
|
|
73
126
|
lines.push(" <url>");
|
|
74
127
|
lines.push(` <loc>${escapeXml(loc)}</loc>`);
|
|
75
128
|
lines.push(` <lastmod>${escapeXml(entry.updatedAt)}</lastmod>`);
|
|
129
|
+
|
|
130
|
+
if (useXhtml && siblings && siblings.length > 1) {
|
|
131
|
+
// Emit one xhtml:link per sibling (including self -- Google
|
|
132
|
+
// recommends including the page's own hreflang annotation).
|
|
133
|
+
// Siblings with unroutable locales are skipped here too.
|
|
134
|
+
for (const sib of siblings) {
|
|
135
|
+
const sibLoc = await resolveEntryUrl(sib);
|
|
136
|
+
if (sibLoc === null) continue;
|
|
137
|
+
lines.push(
|
|
138
|
+
` <xhtml:link rel="alternate" hreflang="${escapeXml(sib.locale)}" href="${escapeXml(sibLoc)}" />`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// x-default: prefer the default-locale sibling, otherwise
|
|
143
|
+
// the first sibling with a routable URL. Stable order:
|
|
144
|
+
// rows arrive sorted by updated_at DESC from the handler.
|
|
145
|
+
const defaultSibling =
|
|
146
|
+
i18nConfig && siblings.find((s) => s.locale === i18nConfig.defaultLocale);
|
|
147
|
+
let xDefaultLoc: string | null = null;
|
|
148
|
+
if (defaultSibling) {
|
|
149
|
+
xDefaultLoc = await resolveEntryUrl(defaultSibling);
|
|
150
|
+
}
|
|
151
|
+
if (xDefaultLoc === null) {
|
|
152
|
+
for (const sib of siblings) {
|
|
153
|
+
const sibLoc = await resolveEntryUrl(sib);
|
|
154
|
+
if (sibLoc !== null) {
|
|
155
|
+
xDefaultLoc = sibLoc;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (xDefaultLoc !== null) {
|
|
161
|
+
lines.push(
|
|
162
|
+
` <xhtml:link rel="alternate" hreflang="x-default" href="${escapeXml(xDefaultLoc)}" />`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
76
167
|
lines.push(" </url>");
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
for (const siblings of groups.values()) {
|
|
171
|
+
for (const entry of siblings) {
|
|
172
|
+
await writeUrl(entry, siblings);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
for (const entry of ungrouped) {
|
|
176
|
+
await writeUrl(entry, null);
|
|
77
177
|
}
|
|
78
178
|
|
|
79
179
|
lines.push("</urlset>");
|
package/src/astro/types.ts
CHANGED
|
@@ -108,6 +108,12 @@ export interface EmDashManifest {
|
|
|
108
108
|
version: string;
|
|
109
109
|
commit?: string;
|
|
110
110
|
hash: string;
|
|
111
|
+
/**
|
|
112
|
+
* Version of Astro the host project is built with. Present when the
|
|
113
|
+
* integration could resolve it. Surfaced so the admin can evaluate a
|
|
114
|
+
* registry plugin's `env:astro` requirement against the running host.
|
|
115
|
+
*/
|
|
116
|
+
astroVersion?: string;
|
|
111
117
|
collections: Record<string, ManifestCollection>;
|
|
112
118
|
plugins: Record<string, ManifestPlugin>;
|
|
113
119
|
/**
|
|
@@ -385,6 +391,14 @@ export interface EmDashHandlers {
|
|
|
385
391
|
request: Request,
|
|
386
392
|
) => Promise<HandlerResponse>;
|
|
387
393
|
|
|
394
|
+
// Public-only plugin API route handler for SSR page components.
|
|
395
|
+
handlePublicPluginApiRoute: (
|
|
396
|
+
pluginId: string,
|
|
397
|
+
method: string,
|
|
398
|
+
path: string,
|
|
399
|
+
request: Request,
|
|
400
|
+
) => Promise<HandlerResponse>;
|
|
401
|
+
|
|
388
402
|
// Plugin route metadata (for auth decisions before dispatch)
|
|
389
403
|
getPluginRouteMeta: (pluginId: string, path: string) => { public: boolean } | null;
|
|
390
404
|
|