emdash 0.30.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/route-utils.mjs +13 -13
- package/dist/api/schemas/index.d.mts +1 -1
- package/dist/api/schemas/index.mjs +3 -3
- package/dist/{api-b8WIiGU4.mjs → api-EibxGP-V.mjs} +23 -23
- package/dist/api-EibxGP-V.mjs.map +1 -0
- package/dist/{apply-CmIJK9j8.mjs → apply-fGHCAwWN.mjs} +26 -25
- package/dist/apply-fGHCAwWN.mjs.map +1 -0
- package/dist/astro/index.d.mts +2 -2
- package/dist/astro/index.mjs +1 -1
- package/dist/astro/middleware/auth.d.mts +2 -2
- package/dist/astro/middleware/auth.mjs +2 -2
- package/dist/astro/middleware/redirect.mjs +5 -5
- package/dist/astro/middleware/request-context.mjs +1 -1
- package/dist/astro/middleware.d.mts +2 -2
- package/dist/astro/middleware.mjs +139 -68
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +6 -6
- package/dist/astro/routes/api/admin/allowed-domains/index.mjs +6 -6
- 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/byline-fields/_slug_/usage.mjs +3 -3
- package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +7 -7
- package/dist/astro/routes/api/admin/byline-fields/index.mjs +7 -7
- package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +7 -7
- package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +14 -14
- package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +14 -14
- package/dist/astro/routes/api/admin/bylines/index.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/bylines/index.mjs +18 -17
- package/dist/astro/routes/api/admin/bylines/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/comments/_id_/status.mjs +10 -10
- package/dist/astro/routes/api/admin/comments/_id_.mjs +5 -5
- package/dist/astro/routes/api/admin/comments/bulk.mjs +9 -9
- package/dist/astro/routes/api/admin/comments/counts.mjs +5 -5
- package/dist/astro/routes/api/admin/comments/index.mjs +9 -9
- 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/media-usage/repair.mjs +10 -10
- 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 +34 -34
- package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +34 -34
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +34 -34
- package/dist/astro/routes/api/admin/plugins/_id_/mcp.mjs +4 -4
- package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs +34 -34
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +34 -34
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +34 -34
- package/dist/astro/routes/api/admin/plugins/index.mjs +34 -34
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +3 -3
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +34 -34
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +34 -34
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +34 -34
- package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +34 -34
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +35 -35
- package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +34 -34
- package/dist/astro/routes/api/admin/plugins/registry/install.mjs +35 -35
- package/dist/astro/routes/api/admin/plugins/updates.mjs +34 -34
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +34 -34
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +3 -3
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +34 -34
- 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 +6 -6
- package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +2 -2
- package/dist/astro/routes/api/admin/users/index.mjs +6 -6
- package/dist/astro/routes/api/auth/dev-bypass.mjs +4 -4
- package/dist/astro/routes/api/auth/invite/accept.mjs +2 -2
- package/dist/astro/routes/api/auth/invite/complete.mjs +6 -6
- package/dist/astro/routes/api/auth/invite/index.mjs +6 -6
- package/dist/astro/routes/api/auth/invite/register-options.mjs +6 -6
- package/dist/astro/routes/api/auth/logout.mjs +2 -2
- package/dist/astro/routes/api/auth/magic-link/send.mjs +7 -7
- package/dist/astro/routes/api/auth/magic-link/verify.mjs +2 -2
- package/dist/astro/routes/api/auth/me.mjs +7 -7
- package/dist/astro/routes/api/auth/mode.mjs +2 -2
- package/dist/astro/routes/api/auth/passkey/_id_.mjs +6 -6
- package/dist/astro/routes/api/auth/passkey/index.mjs +2 -2
- package/dist/astro/routes/api/auth/passkey/options.mjs +7 -7
- package/dist/astro/routes/api/auth/passkey/register/options.mjs +6 -6
- package/dist/astro/routes/api/auth/passkey/register/verify.mjs +6 -6
- package/dist/astro/routes/api/auth/passkey/verify.mjs +6 -6
- package/dist/astro/routes/api/auth/signup/complete.mjs +6 -6
- package/dist/astro/routes/api/auth/signup/request.mjs +7 -7
- package/dist/astro/routes/api/auth/signup/verify.mjs +2 -2
- package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +10 -10
- package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs +10 -10
- 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 +9 -9
- package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +7 -7
- package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs +14 -14
- package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs +14 -14
- 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 +7 -7
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +13 -13
- 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 +7 -7
- package/dist/astro/routes/api/content/_collection_/authors.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/index.mjs +7 -7
- package/dist/astro/routes/api/content/_collection_/trash.mjs +7 -7
- 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 +12 -12
- package/dist/astro/routes/api/import/wordpress/analyze.mjs +3 -3
- package/dist/astro/routes/api/import/wordpress/execute.d.mts +2 -2
- package/dist/astro/routes/api/import/wordpress/execute.mjs +11 -11
- package/dist/astro/routes/api/import/wordpress/media.d.mts +2 -2
- package/dist/astro/routes/api/import/wordpress/media.mjs +7 -7
- package/dist/astro/routes/api/import/wordpress/prepare.mjs +9 -9
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts +2 -2
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +9 -9
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +12 -12
- package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +2 -2
- package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +19 -19
- package/dist/astro/routes/api/manifest.mjs +3 -3
- package/dist/astro/routes/api/mcp.mjs +39 -39
- package/dist/astro/routes/api/media/_id_/confirm.mjs +7 -7
- package/dist/astro/routes/api/media/_id_/usage.mjs +11 -11
- package/dist/astro/routes/api/media/_id_.mjs +10 -10
- 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 +7 -7
- package/dist/astro/routes/api/media.mjs +10 -10
- package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +8 -8
- package/dist/astro/routes/api/menus/_name_/items.mjs +8 -8
- package/dist/astro/routes/api/menus/_name_/reorder.mjs +8 -8
- package/dist/astro/routes/api/menus/_name_/translations.mjs +8 -8
- package/dist/astro/routes/api/menus/_name_.mjs +8 -8
- package/dist/astro/routes/api/menus/index.mjs +8 -8
- package/dist/astro/routes/api/oauth/authorize.d.mts.map +1 -1
- package/dist/astro/routes/api/oauth/authorize.mjs +23 -9
- package/dist/astro/routes/api/oauth/authorize.mjs.map +1 -1
- 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 +4 -4
- package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +4 -4
- package/dist/astro/routes/api/redirects/404s/index.mjs +10 -10
- package/dist/astro/routes/api/redirects/404s/summary.mjs +10 -10
- package/dist/astro/routes/api/redirects/_id_.mjs +11 -11
- package/dist/astro/routes/api/redirects/index.mjs +11 -11
- package/dist/astro/routes/api/relations/_id_/index.mjs +14 -14
- package/dist/astro/routes/api/relations/_id_/translations.mjs +10 -10
- package/dist/astro/routes/api/relations/index.mjs +14 -14
- 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 +34 -34
- package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +34 -34
- package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +34 -34
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +34 -34
- package/dist/astro/routes/api/schema/collections/index.mjs +34 -34
- package/dist/astro/routes/api/schema/index.mjs +8 -8
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs +34 -34
- package/dist/astro/routes/api/schema/orphans/index.mjs +34 -34
- package/dist/astro/routes/api/search/enable.mjs +11 -11
- package/dist/astro/routes/api/search/index.mjs +10 -10
- package/dist/astro/routes/api/search/rebuild.mjs +11 -11
- package/dist/astro/routes/api/search/stats.mjs +7 -7
- package/dist/astro/routes/api/search/suggest.mjs +10 -10
- package/dist/astro/routes/api/sections/_slug_.mjs +10 -10
- package/dist/astro/routes/api/sections/index.mjs +10 -10
- package/dist/astro/routes/api/settings/backups/archives/_name_.mjs +4 -4
- package/dist/astro/routes/api/settings/backups/archives/index.mjs +4 -4
- package/dist/astro/routes/api/settings/backups/export.mjs +4 -4
- package/dist/astro/routes/api/settings/backups/index.mjs +5 -5
- package/dist/astro/routes/api/settings/email.mjs +4 -4
- package/dist/astro/routes/api/settings.mjs +12 -12
- package/dist/astro/routes/api/setup/admin-verify.mjs +6 -6
- package/dist/astro/routes/api/setup/admin.mjs +6 -6
- package/dist/astro/routes/api/setup/dev-bypass.mjs +18 -18
- package/dist/astro/routes/api/setup/dev-reset.mjs +2 -2
- package/dist/astro/routes/api/setup/index.mjs +22 -22
- package/dist/astro/routes/api/setup/status.mjs +3 -3
- package/dist/astro/routes/api/snapshot.mjs +4 -4
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +13 -13
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +13 -13
- package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +13 -13
- package/dist/astro/routes/api/taxonomies/index.mjs +13 -13
- package/dist/astro/routes/api/themes/preview.mjs +4 -4
- package/dist/astro/routes/api/typegen.mjs +5 -5
- package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +7 -7
- package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +10 -10
- package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +10 -10
- package/dist/astro/routes/api/widget-areas/_name_.mjs +6 -6
- package/dist/astro/routes/api/widget-areas/index.mjs +10 -10
- package/dist/astro/routes/api/widget-components.mjs +2 -2
- package/dist/astro/routes/robots.txt.mjs +5 -5
- package/dist/astro/routes/sitemap-_collection_.xml.mjs +7 -7
- package/dist/astro/routes/sitemap.xml.mjs +5 -5
- package/dist/astro/types.d.mts +2 -2
- package/dist/{authorize-WxsiePzy.mjs → authorize-6WPZa1xZ.mjs} +2 -2
- package/dist/{authorize-WxsiePzy.mjs.map → authorize-6WPZa1xZ.mjs.map} +1 -1
- package/dist/{backup-S8kFWtwD.mjs → backup-Qr9PaGUo.mjs} +2 -2
- package/dist/{backup-S8kFWtwD.mjs.map → backup-Qr9PaGUo.mjs.map} +1 -1
- package/dist/{byline-XEjchwzZ.mjs → byline-BTOxUlFF.mjs} +6 -6
- package/dist/{byline-XEjchwzZ.mjs.map → byline-BTOxUlFF.mjs.map} +1 -1
- package/dist/{bylines-czseViYo.mjs → bylines-FyZu6TbJ.mjs} +6 -6
- package/dist/{bylines-czseViYo.mjs.map → bylines-FyZu6TbJ.mjs.map} +1 -1
- package/dist/{bylines-BJbT4gKS.mjs → bylines-oVN0iPwX.mjs} +12 -8
- package/dist/bylines-oVN0iPwX.mjs.map +1 -0
- package/dist/{cache-CGCd6AVM.mjs → cache-DDwy3JDW.mjs} +2 -2
- package/dist/{cache-CGCd6AVM.mjs.map → cache-DDwy3JDW.mjs.map} +1 -1
- package/dist/{chunks-BxXyunY-.mjs → chunks-Bq-5DPrD.mjs} +2 -2
- package/dist/{chunks-BxXyunY-.mjs.map → chunks-Bq-5DPrD.mjs.map} +1 -1
- package/dist/cli/index.mjs +18 -18
- package/dist/{comment-DPT0WKyd.mjs → comment-B9cUud8H.mjs} +2 -2
- package/dist/{comment-DPT0WKyd.mjs.map → comment-B9cUud8H.mjs.map} +1 -1
- package/dist/{comment-reaction-C65MldIB.mjs → comment-reaction-VUXO981F.mjs} +2 -2
- package/dist/{comment-reaction-C65MldIB.mjs.map → comment-reaction-VUXO981F.mjs.map} +1 -1
- package/dist/{comments-Bz6sCbgD.mjs → comments-PXj1E9Si.mjs} +3 -3
- package/dist/{comments-Bz6sCbgD.mjs.map → comments-PXj1E9Si.mjs.map} +1 -1
- package/dist/{config-CVssduLe.mjs → config-B6b_BobR.mjs} +6 -2
- package/dist/config-B6b_BobR.mjs.map +1 -0
- package/dist/{content-Ci04z2z-.mjs → content-BC43pICM.mjs} +4 -4
- package/dist/{content-Ci04z2z-.mjs.map → content-BC43pICM.mjs.map} +1 -1
- package/dist/{content-refresh-D4khvC0R.mjs → content-refresh-CPMSauD3.mjs} +4 -4
- package/dist/{content-refresh-D4khvC0R.mjs.map → content-refresh-CPMSauD3.mjs.map} +1 -1
- package/dist/{context-B6hc7zJL.mjs → context-DOurRIrb.mjs} +10 -10
- package/dist/{context-B6hc7zJL.mjs.map → context-DOurRIrb.mjs.map} +1 -1
- package/dist/{dashboard-C5NkXFbi.mjs → dashboard-CBm4zhc3.mjs} +4 -4
- package/dist/{dashboard-C5NkXFbi.mjs.map → dashboard-CBm4zhc3.mjs.map} +1 -1
- package/dist/database/pg-migration-lock.d.mts.map +1 -1
- package/dist/database/pg-migration-lock.mjs +1 -4
- package/dist/database/pg-migration-lock.mjs.map +1 -1
- package/dist/db/index.mjs +2 -2
- package/dist/{dialect-helpers-HxEQGl_1.mjs → dialect-helpers-BtOwo2us.mjs} +4 -4
- package/dist/dialect-helpers-BtOwo2us.mjs.map +1 -0
- package/dist/{error-CEGF6UZb.mjs → error-DJ98TRbQ.mjs} +2 -2
- package/dist/{error-CEGF6UZb.mjs.map → error-DJ98TRbQ.mjs.map} +1 -1
- package/dist/{field-defs-cache-DvmlgP-D.mjs → field-defs-cache-KfNJ_qLQ.mjs} +2 -2
- package/dist/{field-defs-cache-DvmlgP-D.mjs.map → field-defs-cache-KfNJ_qLQ.mjs.map} +1 -1
- package/dist/{fts-manager-DzqIBrrW.mjs → fts-manager-hyCU9OOp.mjs} +3 -3
- package/dist/{fts-manager-DzqIBrrW.mjs.map → fts-manager-hyCU9OOp.mjs.map} +1 -1
- package/dist/{hreflang-db2Tpb2o.mjs → hreflang-CbYeCzl9.mjs} +7 -7
- package/dist/{hreflang-db2Tpb2o.mjs.map → hreflang-CbYeCzl9.mjs.map} +1 -1
- package/dist/{import-Dmkm8S1W.mjs → import-oDYCgNYQ.mjs} +2 -2
- package/dist/{import-Dmkm8S1W.mjs.map → import-oDYCgNYQ.mjs.map} +1 -1
- package/dist/{index-wYnC6u4o.d.mts → index-BcEH_FfS.d.mts} +33 -2
- package/dist/index-BcEH_FfS.d.mts.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +48 -48
- package/dist/{load-Cx27ki1l.mjs → load-Dg5gtWma.mjs} +2 -2
- package/dist/{load-Cx27ki1l.mjs.map → load-Dg5gtWma.mjs.map} +1 -1
- package/dist/{loader-Be3ouI5L.mjs → loader-D4rigvjR.mjs} +4 -4
- package/dist/{loader-Be3ouI5L.mjs.map → loader-D4rigvjR.mjs.map} +1 -1
- package/dist/media/local-runtime.d.mts +2 -2
- package/dist/media/local-runtime.mjs +5 -5
- package/dist/{media-kIV1IxFf.mjs → media-BJ6SRY3g.mjs} +8 -4
- package/dist/media-BJ6SRY3g.mjs.map +1 -0
- package/dist/{media-BjhhENaJ.mjs → media-its1WLYv.mjs} +2 -2
- package/dist/{media-BjhhENaJ.mjs.map → media-its1WLYv.mjs.map} +1 -1
- package/dist/{media-upload-D2wk3EIt.mjs → media-upload-DNu3QySc.mjs} +4 -4
- package/dist/{media-upload-D2wk3EIt.mjs.map → media-upload-DNu3QySc.mjs.map} +1 -1
- package/dist/{media-usage-CljdO1mc.mjs → media-usage-DPQ3K4xB.mjs} +6 -6
- package/dist/{media-usage-CljdO1mc.mjs.map → media-usage-DPQ3K4xB.mjs.map} +1 -1
- package/dist/{menus-CZyG6rvx.mjs → menus-BMGzWyWH.mjs} +15 -8
- package/dist/menus-BMGzWyWH.mjs.map +1 -0
- package/dist/{menus-BcyElFtI.mjs → menus-CPf_SW7u.mjs} +86 -8
- package/dist/menus-CPf_SW7u.mjs.map +1 -0
- package/dist/{parse-C_-6klII.mjs → parse-53zyxlpv.mjs} +2 -2
- package/dist/{parse-C_-6klII.mjs.map → parse-53zyxlpv.mjs.map} +1 -1
- package/dist/plugin-utils.d.mts +2 -2
- package/dist/plugins/adapt-sandbox-entry.d.mts +2 -2
- package/dist/{query-DR73ZNfm.mjs → query-BbJQ-dGg.mjs} +14 -14
- package/dist/{query-DR73ZNfm.mjs.map → query-BbJQ-dGg.mjs.map} +1 -1
- package/dist/{query-Di7DOmPV.mjs → query-DdFGHzQF.mjs} +3 -3
- package/dist/{query-Di7DOmPV.mjs.map → query-DdFGHzQF.mjs.map} +1 -1
- package/dist/{rate-limit-CMj83JNW.mjs → rate-limit-D3IEiUi2.mjs} +2 -2
- package/dist/{rate-limit-CMj83JNW.mjs.map → rate-limit-D3IEiUi2.mjs.map} +1 -1
- package/dist/{redirect-CgLPYflR.mjs → redirect-BoUac4NT.mjs} +3 -3
- package/dist/{redirect-CgLPYflR.mjs.map → redirect-BoUac4NT.mjs.map} +1 -1
- package/dist/{redirects-B7t9bKsL.mjs → redirects-ChbpqKiS.mjs} +3 -3
- package/dist/{redirects-B7t9bKsL.mjs.map → redirects-ChbpqKiS.mjs.map} +1 -1
- package/dist/{redirects-BBq3n2Rj.mjs → redirects-D457qwt0.mjs} +8 -8
- package/dist/redirects-D457qwt0.mjs.map +1 -0
- package/dist/{registry-FV15nLge.mjs → registry-y21kjhfG.mjs} +6 -6
- package/dist/{registry-FV15nLge.mjs.map → registry-y21kjhfG.mjs.map} +1 -1
- package/dist/{relation-9M9j_UWo.mjs → relation-Du0r04E0.mjs} +3 -3
- package/dist/{relation-9M9j_UWo.mjs.map → relation-Du0r04E0.mjs.map} +1 -1
- package/dist/{relations-Bh8jlFsU.d.mts → relations-BKybBoOX.d.mts} +12 -8
- package/dist/relations-BKybBoOX.d.mts.map +1 -0
- package/dist/{relations-5_avdrN_.mjs → relations-CZMpBqEd.mjs} +3 -3
- package/dist/{relations-5_avdrN_.mjs.map → relations-CZMpBqEd.mjs.map} +1 -1
- package/dist/{relations-BKvO39-9.mjs → relations-DtX1UMBH.mjs} +13 -7
- package/dist/relations-DtX1UMBH.mjs.map +1 -0
- package/dist/{resolve-Cd9dzclN.mjs → resolve-DFr4kq72.mjs} +2 -2
- package/dist/{resolve-Cd9dzclN.mjs.map → resolve-DFr4kq72.mjs.map} +1 -1
- package/dist/{runner-DfnZ5eUr.mjs → runner-BdeLEgad.mjs} +3 -3
- package/dist/{runner-DfnZ5eUr.mjs.map → runner-BdeLEgad.mjs.map} +1 -1
- package/dist/runtime.d.mts +2 -2
- package/dist/runtime.mjs +3 -3
- package/dist/{schema-BXxlHeAf.mjs → schema-BH8P8vaC.mjs} +3 -3
- package/dist/{schema-BXxlHeAf.mjs.map → schema-BH8P8vaC.mjs.map} +1 -1
- package/dist/{scopes-Bl4IwHA-.mjs → scopes-DTIkIaGA.mjs} +2 -2
- package/dist/{scopes-Bl4IwHA-.mjs.map → scopes-DTIkIaGA.mjs.map} +1 -1
- package/dist/{search-Bff-7jFt.mjs → search-D-Yhpmvs.mjs} +8 -7
- package/dist/search-D-Yhpmvs.mjs.map +1 -0
- package/dist/{secrets-CSwQIl4q.mjs → secrets-CQ-69kF9.mjs} +23 -17
- package/dist/secrets-CQ-69kF9.mjs.map +1 -0
- package/dist/{sections-CwW4s1al.mjs → sections-CzVBAKjw.mjs} +3 -3
- package/dist/{sections-CwW4s1al.mjs.map → sections-CzVBAKjw.mjs.map} +1 -1
- package/dist/seed/index.mjs +16 -16
- package/dist/seo/index.mjs +12 -12
- package/dist/{seo-2cmFHZns.mjs → seo-kJzVAmmu.mjs} +2 -2
- package/dist/{seo-2cmFHZns.mjs.map → seo-kJzVAmmu.mjs.map} +1 -1
- package/dist/{service-DkGTWGIi.mjs → service-CQnc276a.mjs} +2 -2
- package/dist/{service-DkGTWGIi.mjs.map → service-CQnc276a.mjs.map} +1 -1
- package/dist/{settings-CpA4lQFt.mjs → settings-C2PaBS49.mjs} +3 -3
- package/dist/{settings-CpA4lQFt.mjs.map → settings-C2PaBS49.mjs.map} +1 -1
- package/dist/{settings-C4s8hFQm.mjs → settings-Dtc0DU31.mjs} +3 -3
- package/dist/{settings-C4s8hFQm.mjs.map → settings-Dtc0DU31.mjs.map} +1 -1
- package/dist/{taxonomies-DjSKBZpq.mjs → taxonomies-HI9fnTdp.mjs} +7 -7
- package/dist/{taxonomies-DjSKBZpq.mjs.map → taxonomies-HI9fnTdp.mjs.map} +1 -1
- package/dist/{taxonomies-Ce49uIzY.mjs → taxonomies-rMeBzOjX.mjs} +26 -17
- package/dist/taxonomies-rMeBzOjX.mjs.map +1 -0
- package/dist/{taxonomy-DfVooU4W.mjs → taxonomy-0mCI25HN.mjs} +3 -3
- package/dist/{taxonomy-DfVooU4W.mjs.map → taxonomy-0mCI25HN.mjs.map} +1 -1
- package/dist/{types-D1iJ3DpO.mjs → types-CrRUFdej.mjs} +2 -2
- package/dist/{types-D1iJ3DpO.mjs.map → types-CrRUFdej.mjs.map} +1 -1
- package/dist/{user-Bh-L1qo6.mjs → user-aTXqmXBz.mjs} +3 -3
- package/dist/{user-Bh-L1qo6.mjs.map → user-aTXqmXBz.mjs.map} +1 -1
- package/dist/{validate-V9nCwq_-.mjs → validate-CDT1jl6_.mjs} +11 -6
- package/dist/validate-CDT1jl6_.mjs.map +1 -0
- package/dist/validate-DULfgpJP.d.mts.map +1 -1
- package/dist/{validation-BsVUJfsP.mjs → validation-DlD0E35W.mjs} +8 -8
- package/dist/{validation-BsVUJfsP.mjs.map → validation-DlD0E35W.mjs.map} +1 -1
- package/dist/version-Ci6bw6YK.mjs +7 -0
- package/dist/{version-907opKac.mjs.map → version-Ci6bw6YK.mjs.map} +1 -1
- package/dist/{widgets-DGv1Z04V.mjs → widgets-5fipNkMT.mjs} +2 -2
- package/dist/{widgets-DGv1Z04V.mjs.map → widgets-5fipNkMT.mjs.map} +1 -1
- package/dist/{wxr-taxonomies-Cs1N6O8z.mjs → wxr-taxonomies-BmUnBNgz.mjs} +4 -4
- package/dist/{wxr-taxonomies-Cs1N6O8z.mjs.map → wxr-taxonomies-BmUnBNgz.mjs.map} +1 -1
- package/dist/{zod-generator-B5prQ5M4.mjs → zod-generator-CwkHWz4f.mjs} +2 -2
- package/dist/{zod-generator-B5prQ5M4.mjs.map → zod-generator-CwkHWz4f.mjs.map} +1 -1
- package/package.json +5 -5
- package/src/api/handlers/bylines.ts +5 -4
- package/src/api/handlers/content.ts +25 -7
- package/src/api/handlers/menus.ts +11 -6
- package/src/api/handlers/relations.ts +7 -2
- package/src/api/handlers/taxonomies.ts +22 -14
- package/src/api/schemas/bylines.ts +4 -4
- package/src/api/schemas/common.ts +7 -6
- package/src/api/schemas/menus.ts +2 -1
- package/src/api/schemas/relations.ts +3 -1
- package/src/api/schemas/taxonomies.ts +4 -2
- package/src/astro/routes/api/admin/bylines/index.ts +5 -8
- package/src/astro/routes/api/oauth/authorize.ts +42 -10
- package/src/components/Gallery.astro +141 -26
- package/src/config/secrets.ts +25 -19
- package/src/content/converters/gallery.ts +61 -0
- package/src/content/converters/portable-text-to-prosemirror.ts +19 -0
- package/src/content/converters/prosemirror-to-portable-text.ts +18 -0
- package/src/content/converters/types.ts +36 -0
- package/src/database/dialect-helpers.ts +3 -4
- package/src/database/pg-migration-lock.ts +1 -4
- package/src/emdash-runtime.ts +55 -17
- package/src/i18n/config.ts +8 -0
- package/src/i18n/repair-locale-casing.ts +51 -0
- package/src/search/query.ts +3 -2
- package/src/seed/apply.ts +9 -7
- package/src/seed/validate.ts +15 -4
- package/dist/api-b8WIiGU4.mjs.map +0 -1
- package/dist/apply-CmIJK9j8.mjs.map +0 -1
- package/dist/bylines-BJbT4gKS.mjs.map +0 -1
- package/dist/config-CVssduLe.mjs.map +0 -1
- package/dist/dialect-helpers-HxEQGl_1.mjs.map +0 -1
- package/dist/index-wYnC6u4o.d.mts.map +0 -1
- package/dist/media-kIV1IxFf.mjs.map +0 -1
- package/dist/menus-BcyElFtI.mjs.map +0 -1
- package/dist/menus-CZyG6rvx.mjs.map +0 -1
- package/dist/redirects-BBq3n2Rj.mjs.map +0 -1
- package/dist/relations-BKvO39-9.mjs.map +0 -1
- package/dist/relations-Bh8jlFsU.d.mts.map +0 -1
- package/dist/search-Bff-7jFt.mjs.map +0 -1
- package/dist/secrets-CSwQIl4q.mjs.map +0 -1
- package/dist/taxonomies-Ce49uIzY.mjs.map +0 -1
- package/dist/validate-V9nCwq_-.mjs.map +0 -1
- package/dist/version-907opKac.mjs +0 -7
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"menus-CZyG6rvx.mjs","names":[],"sources":["../src/database/repositories/menu.ts","../src/api/handlers/menus.ts"],"sourcesContent":["/**\n * Menu repository\n *\n * Owns every SQL touch for `_emdash_menus` and `_emdash_menu_items`, plus the\n * row→entity mapping. Matches the architecture used by every other resource\n * (content, taxonomies, redirects, comments, media): handlers stay thin and\n * orchestrate; the repository is the single place where snake_case DB columns\n * become camelCase entities (and vice versa).\n *\n * i18n: menus are per-locale. `(name, locale)` is unique. Translations of the\n * same menu share a `translation_group` ULID. Menu item `reference_id` stores\n * the referenced content's translation_group (not a specific row id) so a\n * single menu item survives content translations.\n */\n\nimport type { Kysely, Selectable } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport { invalidateMenuObjectCache } from \"../../object-cache/index.js\";\nimport { withTransaction } from \"../transaction.js\";\nimport type { Database, MenuItemTable, MenuTable } from \"../types.js\";\n\n/**\n * Thrown from inside a repository transaction when the menu the caller\n * resolved earlier has since been deleted. Handlers translate this to a\n * `NOT_FOUND` API response. Necessary because D1 disables FK enforcement\n * (so `ON DELETE CASCADE` won't fire), and an unchecked `setItems` would\n * happily insert items whose `menu_id` no longer exists, leaving orphans.\n */\nexport class MenuGoneError extends Error {\n\tconstructor(public readonly menuId: string) {\n\t\tsuper(`Menu ${menuId} was deleted while being modified`);\n\t\tthis.name = \"MenuGoneError\";\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Entity shapes (camelCase — what the API returns)\n// ---------------------------------------------------------------------------\n\nexport interface Menu {\n\tid: string;\n\tname: string;\n\tlabel: string;\n\tcreatedAt: string;\n\tupdatedAt: string;\n\tlocale: string;\n\ttranslationGroup: string | null;\n}\n\nexport interface MenuItem {\n\tid: string;\n\tmenuId: string;\n\tparentId: string | null;\n\tsortOrder: number;\n\ttype: string;\n\treferenceCollection: string | null;\n\treferenceId: string | null;\n\tcustomUrl: string | null;\n\tlabel: string;\n\ttitleAttr: string | null;\n\ttarget: string | null;\n\tcssClasses: string | null;\n\tcreatedAt: string;\n\tlocale: string;\n\ttranslationGroup: string | null;\n}\n\nexport interface MenuListItem extends Menu {\n\titemCount: number;\n}\n\nexport interface MenuWithItems extends Menu {\n\titems: MenuItem[];\n}\n\nexport interface MenuTranslation {\n\tid: string;\n\tname: string;\n\tlabel: string;\n\tlocale: string;\n\tupdatedAt: string;\n}\n\n// ---------------------------------------------------------------------------\n// Input shapes\n// ---------------------------------------------------------------------------\n\nexport interface CreateMenuInput {\n\tname: string;\n\tlabel: string;\n\tlocale?: string;\n\t/**\n\t * When set, the new menu joins the source menu's translation_group and\n\t * inherits its items (cloned, with new ULIDs but the same translation_group\n\t * per item so nav entries stay logically identified across translations).\n\t */\n\ttranslationOf?: string;\n}\n\nexport interface UpdateMenuInput {\n\tlabel?: string;\n}\n\nexport interface CreateMenuItemInput {\n\ttype: string;\n\tlabel: string;\n\treferenceCollection?: string;\n\treferenceId?: string;\n\tcustomUrl?: string;\n\ttarget?: string;\n\ttitleAttr?: string;\n\tcssClasses?: string;\n\tparentId?: string;\n\tsortOrder?: number;\n}\n\nexport interface UpdateMenuItemInput {\n\tlabel?: string;\n\tcustomUrl?: string;\n\ttarget?: string;\n\ttitleAttr?: string;\n\tcssClasses?: string;\n\tparentId?: string | null;\n\tsortOrder?: number;\n}\n\n/**\n * Item shape used by `setItems()`. Items are placed by array order. Children\n * point at parents via `parentIndex` (must reference an earlier index, so the\n * insert can resolve parents before children). The validation of that ordering\n * lives at the API boundary (`handleMenuSetItems`) so REST/MCP callers receive\n * the same error shape.\n */\nexport interface SetMenuItem {\n\tlabel: string;\n\ttype: \"custom\" | \"page\" | \"post\" | \"taxonomy\" | \"collection\";\n\tcustomUrl?: string;\n\treferenceCollection?: string;\n\treferenceId?: string;\n\ttitleAttr?: string;\n\ttarget?: string;\n\tcssClasses?: string;\n\tparentIndex?: number;\n}\n\nexport interface ReorderItem {\n\tid: string;\n\tparentId: string | null;\n\tsortOrder: number;\n}\n\n// ---------------------------------------------------------------------------\n// Row → entity mappers\n// ---------------------------------------------------------------------------\n\nfunction rowToMenu(row: Selectable<MenuTable>): Menu {\n\treturn {\n\t\tid: row.id,\n\t\tname: row.name,\n\t\tlabel: row.label,\n\t\tcreatedAt: row.created_at,\n\t\tupdatedAt: row.updated_at,\n\t\tlocale: row.locale,\n\t\ttranslationGroup: row.translation_group,\n\t};\n}\n\nfunction rowToMenuItem(row: Selectable<MenuItemTable>): MenuItem {\n\treturn {\n\t\tid: row.id,\n\t\tmenuId: row.menu_id,\n\t\tparentId: row.parent_id,\n\t\tsortOrder: row.sort_order,\n\t\ttype: row.type,\n\t\treferenceCollection: row.reference_collection,\n\t\treferenceId: row.reference_id,\n\t\tcustomUrl: row.custom_url,\n\t\tlabel: row.label,\n\t\ttitleAttr: row.title_attr,\n\t\ttarget: row.target,\n\t\tcssClasses: row.css_classes,\n\t\tcreatedAt: row.created_at,\n\t\tlocale: row.locale,\n\t\ttranslationGroup: row.translation_group,\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Repository\n// ---------------------------------------------------------------------------\n\nexport class MenuRepository {\n\tconstructor(private db: Kysely<Database>) {}\n\n\t// --- Menus -------------------------------------------------------------\n\n\t/**\n\t * List menus with their item counts. When `locale` is omitted, returns\n\t * every locale variant as its own row (consistent with the admin listing\n\t * model: each translation is its own menu for editing purposes).\n\t */\n\tasync findMany(options: { locale?: string } = {}): Promise<MenuListItem[]> {\n\t\t// Single LEFT JOIN + GROUP BY for the per-menu count. Avoids N+1.\n\t\tlet query = this.db\n\t\t\t.selectFrom(\"_emdash_menus as m\")\n\t\t\t.leftJoin(\"_emdash_menu_items as i\", \"i.menu_id\", \"m.id\")\n\t\t\t.select(({ fn }) => [\n\t\t\t\t\"m.id\",\n\t\t\t\t\"m.name\",\n\t\t\t\t\"m.label\",\n\t\t\t\t\"m.created_at\",\n\t\t\t\t\"m.updated_at\",\n\t\t\t\t\"m.locale\",\n\t\t\t\t\"m.translation_group\",\n\t\t\t\tfn.count<number>(\"i.id\").as(\"itemCount\"),\n\t\t\t])\n\t\t\t.groupBy([\n\t\t\t\t\"m.id\",\n\t\t\t\t\"m.name\",\n\t\t\t\t\"m.label\",\n\t\t\t\t\"m.created_at\",\n\t\t\t\t\"m.updated_at\",\n\t\t\t\t\"m.locale\",\n\t\t\t\t\"m.translation_group\",\n\t\t\t])\n\t\t\t.orderBy(\"m.name\", \"asc\");\n\t\tif (options.locale !== undefined) query = query.where(\"m.locale\", \"=\", options.locale);\n\t\tconst rows = await query.execute();\n\n\t\treturn rows.map((row) => ({\n\t\t\t// Postgres returns count() as `string`; SQLite as `number`. Normalize.\n\t\t\titemCount: typeof row.itemCount === \"string\" ? Number(row.itemCount) : row.itemCount,\n\t\t\t...rowToMenu({\n\t\t\t\tid: row.id,\n\t\t\t\tname: row.name,\n\t\t\t\tlabel: row.label,\n\t\t\t\tcreated_at: row.created_at,\n\t\t\t\tupdated_at: row.updated_at,\n\t\t\t\tlocale: row.locale,\n\t\t\t\ttranslation_group: row.translation_group,\n\t\t\t}),\n\t\t}));\n\t}\n\n\t/**\n\t * Find every menu row matching `name` (one per locale on multi-locale\n\t * installs). Callers use this both to look up a single menu (when locale\n\t * is supplied) and to detect AMBIGUOUS_LOCALE situations (`length > 1`).\n\t */\n\tasync findByName(name: string, options: { locale?: string } = {}): Promise<Menu[]> {\n\t\tlet query = this.db\n\t\t\t.selectFrom(\"_emdash_menus\")\n\t\t\t.selectAll()\n\t\t\t.where(\"name\", \"=\", name)\n\t\t\t.orderBy(\"locale\", \"asc\");\n\t\tif (options.locale !== undefined) query = query.where(\"locale\", \"=\", options.locale);\n\t\tconst rows = await query.execute();\n\t\treturn rows.map(rowToMenu);\n\t}\n\n\tasync findById(id: string): Promise<Menu | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_emdash_menus\")\n\t\t\t.selectAll()\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.executeTakeFirst();\n\t\treturn row ? rowToMenu(row) : null;\n\t}\n\n\t/** Fetch a menu plus its items, ordered by `sort_order`. */\n\tasync findWithItems(menuId: string): Promise<MenuWithItems | null> {\n\t\tconst menu = await this.findById(menuId);\n\t\tif (!menu) return null;\n\t\tconst items = await this.findItems(menuId);\n\t\treturn { ...menu, items };\n\t}\n\n\tasync findItems(menuId: string): Promise<MenuItem[]> {\n\t\tconst rows = await this.db\n\t\t\t.selectFrom(\"_emdash_menu_items\")\n\t\t\t.selectAll()\n\t\t\t.where(\"menu_id\", \"=\", menuId)\n\t\t\t.orderBy(\"sort_order\", \"asc\")\n\t\t\t.execute();\n\t\treturn rows.map(rowToMenuItem);\n\t}\n\n\t/**\n\t * Returns true when a menu already exists for the given `(name, locale)`.\n\t * Used by the handler to surface a CONFLICT before attempting the insert.\n\t */\n\tasync existsByNameAndLocale(name: string, locale: string): Promise<boolean> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_emdash_menus\")\n\t\t\t.select(\"id\")\n\t\t\t.where(\"name\", \"=\", name)\n\t\t\t.where(\"locale\", \"=\", locale)\n\t\t\t.executeTakeFirst();\n\t\treturn row !== undefined;\n\t}\n\n\t/**\n\t * Create a menu. When `translationOf` is supplied the new menu joins the\n\t * source menu's translation_group and clones its items (each clone gets a\n\t * fresh ULID, but inherits the source item's `translation_group` so a\n\t * given nav entry resolves to \"the same item\" across menu translations).\n\t *\n\t * If the source menu is missing this throws — callers should validate\n\t * existence via `findById` first to return a clean NOT_FOUND.\n\t */\n\tasync create(input: CreateMenuInput): Promise<Menu> {\n\t\tconst id = ulid();\n\n\t\tlet translationGroup: string = id;\n\t\tlet sourceMenuId: string | null = null;\n\t\tif (input.translationOf) {\n\t\t\tconst source = await this.findById(input.translationOf);\n\t\t\tif (!source) throw new Error(\"Source menu for translation not found\");\n\t\t\ttranslationGroup = source.translationGroup ?? source.id;\n\t\t\tsourceMenuId = source.id;\n\t\t}\n\n\t\tawait withTransaction(this.db, async (trx) => {\n\t\t\tawait trx\n\t\t\t\t.insertInto(\"_emdash_menus\")\n\t\t\t\t.values({\n\t\t\t\t\tid,\n\t\t\t\t\tname: input.name,\n\t\t\t\t\tlabel: input.label,\n\t\t\t\t\t...(input.locale !== undefined ? { locale: input.locale } : {}),\n\t\t\t\t\ttranslation_group: translationGroup,\n\t\t\t\t})\n\t\t\t\t.execute();\n\n\t\t\tif (sourceMenuId) {\n\t\t\t\tconst sourceItems = await trx\n\t\t\t\t\t.selectFrom(\"_emdash_menu_items\")\n\t\t\t\t\t.selectAll()\n\t\t\t\t\t.where(\"menu_id\", \"=\", sourceMenuId)\n\t\t\t\t\t.orderBy(\"sort_order\", \"asc\")\n\t\t\t\t\t.execute();\n\t\t\t\tif (sourceItems.length > 0) {\n\t\t\t\t\t// old-id → new-id map so parent pointers land on the clones.\n\t\t\t\t\tconst idMap = new Map<string, string>();\n\t\t\t\t\tfor (const item of sourceItems) idMap.set(item.id, ulid());\n\n\t\t\t\t\tawait trx\n\t\t\t\t\t\t.insertInto(\"_emdash_menu_items\")\n\t\t\t\t\t\t.values(\n\t\t\t\t\t\t\tsourceItems.map((item) => ({\n\t\t\t\t\t\t\t\tid: idMap.get(item.id)!,\n\t\t\t\t\t\t\t\tmenu_id: id,\n\t\t\t\t\t\t\t\tparent_id: item.parent_id ? (idMap.get(item.parent_id) ?? null) : null,\n\t\t\t\t\t\t\t\tsort_order: item.sort_order,\n\t\t\t\t\t\t\t\ttype: item.type,\n\t\t\t\t\t\t\t\treference_collection: item.reference_collection,\n\t\t\t\t\t\t\t\treference_id: item.reference_id,\n\t\t\t\t\t\t\t\tcustom_url: item.custom_url,\n\t\t\t\t\t\t\t\tlabel: item.label,\n\t\t\t\t\t\t\t\ttitle_attr: item.title_attr,\n\t\t\t\t\t\t\t\ttarget: item.target,\n\t\t\t\t\t\t\t\tcss_classes: item.css_classes,\n\t\t\t\t\t\t\t\t...(input.locale !== undefined ? { locale: input.locale } : {}),\n\t\t\t\t\t\t\t\ttranslation_group: item.translation_group ?? item.id,\n\t\t\t\t\t\t\t})),\n\t\t\t\t\t\t)\n\t\t\t\t\t\t.execute();\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\n\t\tinvalidateMenuObjectCache();\n\n\t\tconst created = await this.findById(id);\n\t\tif (!created) throw new Error(\"Failed to create menu\");\n\t\treturn created;\n\t}\n\n\tasync update(id: string, input: UpdateMenuInput): Promise<Menu | null> {\n\t\tconst existing = await this.findById(id);\n\t\tif (!existing) return null;\n\n\t\tconst values: Record<string, unknown> = {};\n\t\tif (input.label !== undefined) values.label = input.label;\n\n\t\tif (Object.keys(values).length > 0) {\n\t\t\tawait this.db.updateTable(\"_emdash_menus\").set(values).where(\"id\", \"=\", id).execute();\n\t\t\tinvalidateMenuObjectCache();\n\t\t}\n\n\t\treturn (await this.findById(id))!;\n\t}\n\n\t/**\n\t * Delete a menu. Items are deleted explicitly to avoid relying on the\n\t * `ON DELETE CASCADE` FK declared in migration 005, which migration 036\n\t * removed: that FK is what made #1021 destructive on D1 (the cascade\n\t * fired when the i18n migration dropped `_emdash_menus`), so dropping\n\t * the FK was the fix. The explicit delete keeps the runtime working\n\t * the same way before and after the migration.\n\t */\n\tasync delete(id: string): Promise<boolean> {\n\t\tconst existing = await this.findById(id);\n\t\tif (!existing) return false;\n\n\t\tawait withTransaction(this.db, async (trx) => {\n\t\t\tawait trx.deleteFrom(\"_emdash_menu_items\").where(\"menu_id\", \"=\", id).execute();\n\t\t\tawait trx.deleteFrom(\"_emdash_menus\").where(\"id\", \"=\", id).execute();\n\t\t});\n\t\tinvalidateMenuObjectCache();\n\t\treturn true;\n\t}\n\n\t/**\n\t * List every translation of a menu (by id or translation_group).\n\t *\n\t * Returns `null` when neither the id nor the group resolves to a menu,\n\t * mapped to NOT_FOUND by the handler.\n\t */\n\tasync listTranslations(\n\t\tidOrGroup: string,\n\t): Promise<{ translationGroup: string | null; translations: MenuTranslation[] } | null> {\n\t\tconst anchor = await this.db\n\t\t\t.selectFrom(\"_emdash_menus\")\n\t\t\t.selectAll()\n\t\t\t.where((eb) => eb.or([eb(\"id\", \"=\", idOrGroup), eb(\"translation_group\", \"=\", idOrGroup)]))\n\t\t\t.executeTakeFirst();\n\t\tif (!anchor) return null;\n\n\t\tconst group = anchor.translation_group ?? anchor.id;\n\t\tconst rows = await this.db\n\t\t\t.selectFrom(\"_emdash_menus\")\n\t\t\t.selectAll()\n\t\t\t.where(\"translation_group\", \"=\", group)\n\t\t\t.orderBy(\"locale\", \"asc\")\n\t\t\t.execute();\n\n\t\treturn {\n\t\t\ttranslationGroup: group,\n\t\t\ttranslations: rows.map((row) => ({\n\t\t\t\tid: row.id,\n\t\t\t\tname: row.name,\n\t\t\t\tlocale: row.locale,\n\t\t\t\tlabel: row.label,\n\t\t\t\tupdatedAt: row.updated_at,\n\t\t\t})),\n\t\t};\n\t}\n\n\t// --- Items -------------------------------------------------------------\n\n\t/**\n\t * Insert a menu item. `locale` is propagated from the parent menu so\n\t * `_emdash_menu_items.locale` mirrors the menu's locale (queries can scope\n\t * by locale without a join).\n\t *\n\t * When `sortOrder` is omitted, the next position within the same parent\n\t * scope is used (max + 1). The fresh `translation_group` defaults to the\n\t * item's own id, matching the migration 036 backfill.\n\t */\n\tasync createItem(menuId: string, locale: string, input: CreateMenuItemInput): Promise<MenuItem> {\n\t\tlet sortOrder = input.sortOrder ?? 0;\n\t\tif (input.sortOrder === undefined) {\n\t\t\tconst maxOrder = await this.db\n\t\t\t\t.selectFrom(\"_emdash_menu_items\")\n\t\t\t\t.select(({ fn }) => fn.max(\"sort_order\").as(\"max\"))\n\t\t\t\t.where(\"menu_id\", \"=\", menuId)\n\t\t\t\t.where(\"parent_id\", \"is\", input.parentId ?? null)\n\t\t\t\t.executeTakeFirst();\n\t\t\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- Kysely fn.max returns unknown; always a number for sort_order column\n\t\t\tsortOrder = ((maxOrder?.max as number) ?? -1) + 1;\n\t\t}\n\n\t\tconst id = ulid();\n\t\tawait this.db\n\t\t\t.insertInto(\"_emdash_menu_items\")\n\t\t\t.values({\n\t\t\t\tid,\n\t\t\t\tmenu_id: menuId,\n\t\t\t\tparent_id: input.parentId ?? null,\n\t\t\t\tsort_order: sortOrder,\n\t\t\t\ttype: input.type,\n\t\t\t\treference_collection: input.referenceCollection ?? null,\n\t\t\t\treference_id: input.referenceId ?? null,\n\t\t\t\tcustom_url: input.customUrl ?? null,\n\t\t\t\tlabel: input.label,\n\t\t\t\ttitle_attr: input.titleAttr ?? null,\n\t\t\t\ttarget: input.target ?? null,\n\t\t\t\tcss_classes: input.cssClasses ?? null,\n\t\t\t\tlocale,\n\t\t\t\ttranslation_group: id,\n\t\t\t})\n\t\t\t.execute();\n\n\t\tinvalidateMenuObjectCache();\n\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_emdash_menu_items\")\n\t\t\t.selectAll()\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.executeTakeFirstOrThrow();\n\t\treturn rowToMenuItem(row);\n\t}\n\n\t/**\n\t * Update a menu item. Caller must ensure the item belongs to the menu —\n\t * the `where(\"menu_id\", \"=\", menuId)` guard prevents cross-menu writes.\n\t * Returns `null` if the item is not found within the menu.\n\t */\n\tasync updateItem(\n\t\tmenuId: string,\n\t\titemId: string,\n\t\tinput: UpdateMenuItemInput,\n\t): Promise<MenuItem | null> {\n\t\tconst existing = await this.db\n\t\t\t.selectFrom(\"_emdash_menu_items\")\n\t\t\t.select(\"id\")\n\t\t\t.where(\"id\", \"=\", itemId)\n\t\t\t.where(\"menu_id\", \"=\", menuId)\n\t\t\t.executeTakeFirst();\n\t\tif (!existing) return null;\n\n\t\tconst values: Record<string, unknown> = {};\n\t\tif (input.label !== undefined) values.label = input.label;\n\t\tif (input.customUrl !== undefined) values.custom_url = input.customUrl;\n\t\tif (input.target !== undefined) values.target = input.target;\n\t\tif (input.titleAttr !== undefined) values.title_attr = input.titleAttr;\n\t\tif (input.cssClasses !== undefined) values.css_classes = input.cssClasses;\n\t\tif (input.parentId !== undefined) values.parent_id = input.parentId;\n\t\tif (input.sortOrder !== undefined) values.sort_order = input.sortOrder;\n\n\t\tif (Object.keys(values).length > 0) {\n\t\t\tawait this.db\n\t\t\t\t.updateTable(\"_emdash_menu_items\")\n\t\t\t\t.set(values)\n\t\t\t\t.where(\"id\", \"=\", itemId)\n\t\t\t\t.execute();\n\t\t\tinvalidateMenuObjectCache();\n\t\t}\n\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_emdash_menu_items\")\n\t\t\t.selectAll()\n\t\t\t.where(\"id\", \"=\", itemId)\n\t\t\t.executeTakeFirstOrThrow();\n\t\treturn rowToMenuItem(row);\n\t}\n\n\t/** Delete an item scoped to its menu. Returns false if nothing was deleted. */\n\tasync deleteItem(menuId: string, itemId: string): Promise<boolean> {\n\t\tconst result = await this.db\n\t\t\t.deleteFrom(\"_emdash_menu_items\")\n\t\t\t.where(\"id\", \"=\", itemId)\n\t\t\t.where(\"menu_id\", \"=\", menuId)\n\t\t\t.execute();\n\t\tconst deleted = result[0]?.numDeletedRows !== 0n;\n\t\tif (deleted) invalidateMenuObjectCache();\n\t\treturn deleted;\n\t}\n\n\t/**\n\t * Atomic replace: delete every existing item and re-insert in order.\n\t * `parentIndex` (validated by the caller) is resolved against the live\n\t * insert order so children always reference real parent ids.\n\t *\n\t * Returns the count of inserted items (matches the existing handler API).\n\t */\n\tasync setItems(\n\t\tmenuId: string,\n\t\tlocale: string,\n\t\titems: SetMenuItem[],\n\t): Promise<{ itemCount: number }> {\n\t\tawait withTransaction(this.db, async (trx) => {\n\t\t\t// Re-check menu existence INSIDE the transaction. The handler\n\t\t\t// resolved by (name, locale) before this call; if a concurrent\n\t\t\t// menu_delete landed in between, inserting new items would\n\t\t\t// silently orphan them. The FK from migration 005 was removed\n\t\t\t// by migration 036 (#1021) and not restored, so nothing at the\n\t\t\t// schema level stops the orphans. Throw a MenuGoneError so the\n\t\t\t// rollback fires and the handler returns NOT_FOUND with the\n\t\t\t// original menu name in the message.\n\t\t\tconst stillThere = await trx\n\t\t\t\t.selectFrom(\"_emdash_menus\")\n\t\t\t\t.select(\"id\")\n\t\t\t\t.where(\"id\", \"=\", menuId)\n\t\t\t\t.executeTakeFirst();\n\t\t\tif (!stillThere) throw new MenuGoneError(menuId);\n\n\t\t\tawait trx.deleteFrom(\"_emdash_menu_items\").where(\"menu_id\", \"=\", menuId).execute();\n\n\t\t\tconst insertedIds: string[] = [];\n\t\t\tfor (let i = 0; i < items.length; i++) {\n\t\t\t\tconst item = items[i];\n\t\t\t\tif (!item) continue;\n\t\t\t\tconst id = ulid();\n\t\t\t\tconst parentId =\n\t\t\t\t\titem.parentIndex !== undefined ? (insertedIds[item.parentIndex] ?? null) : null;\n\t\t\t\tawait trx\n\t\t\t\t\t.insertInto(\"_emdash_menu_items\")\n\t\t\t\t\t.values({\n\t\t\t\t\t\tid,\n\t\t\t\t\t\tmenu_id: menuId,\n\t\t\t\t\t\tparent_id: parentId,\n\t\t\t\t\t\tsort_order: i,\n\t\t\t\t\t\ttype: item.type,\n\t\t\t\t\t\treference_collection: item.referenceCollection ?? null,\n\t\t\t\t\t\treference_id: item.referenceId ?? null,\n\t\t\t\t\t\tcustom_url: item.customUrl ?? null,\n\t\t\t\t\t\tlabel: item.label,\n\t\t\t\t\t\ttitle_attr: item.titleAttr ?? null,\n\t\t\t\t\t\ttarget: item.target ?? null,\n\t\t\t\t\t\tcss_classes: item.cssClasses ?? null,\n\t\t\t\t\t\tlocale,\n\t\t\t\t\t})\n\t\t\t\t\t.execute();\n\t\t\t\tinsertedIds.push(id);\n\t\t\t}\n\n\t\t\tawait trx\n\t\t\t\t.updateTable(\"_emdash_menus\")\n\t\t\t\t.set({ updated_at: new Date().toISOString() })\n\t\t\t\t.where(\"id\", \"=\", menuId)\n\t\t\t\t.execute();\n\t\t});\n\n\t\tinvalidateMenuObjectCache();\n\t\treturn { itemCount: items.length };\n\t}\n\n\t/**\n\t * Batch reorder items. Each entry is applied scoped to the menu so a\n\t * malicious payload cannot move foreign items into this menu's siblings.\n\t */\n\tasync reorderItems(menuId: string, items: ReorderItem[]): Promise<MenuItem[]> {\n\t\tinvalidateMenuObjectCache();\n\t\treturn withTransaction(this.db, async (trx) => {\n\t\t\tfor (const item of items) {\n\t\t\t\tawait trx\n\t\t\t\t\t.updateTable(\"_emdash_menu_items\")\n\t\t\t\t\t.set({ parent_id: item.parentId, sort_order: item.sortOrder })\n\t\t\t\t\t.where(\"id\", \"=\", item.id)\n\t\t\t\t\t.where(\"menu_id\", \"=\", menuId)\n\t\t\t\t\t.execute();\n\t\t\t}\n\n\t\t\tconst rows = await trx\n\t\t\t\t.selectFrom(\"_emdash_menu_items\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"menu_id\", \"=\", menuId)\n\t\t\t\t.orderBy(\"sort_order\", \"asc\")\n\t\t\t\t.execute();\n\t\t\treturn rows.map(rowToMenuItem);\n\t\t});\n\t}\n}\n","/**\n * Menu CRUD handlers.\n *\n * Business logic for menu and menu-item endpoints. Routes are thin wrappers\n * that parse input, check auth, and call these.\n *\n * i18n: Menus are per-locale. `(name, locale)` is unique, so the same `name`\n * (e.g. \"primary\") can exist in several locales within one translation_group.\n * Menu items carry a `locale` + `translation_group` as well, and their\n * `reference_id` points at the referenced content's translation_group (not a\n * specific row id), so a single menu item target survives content translations.\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport {\n\tMenuGoneError,\n\tMenuRepository,\n\ttype CreateMenuItemInput as CreateMenuItemRepoInput,\n\ttype Menu,\n\ttype MenuItem,\n\ttype MenuListItem,\n\ttype MenuWithItems,\n\ttype SetMenuItem,\n\ttype UpdateMenuItemInput as UpdateMenuItemRepoInput,\n} from \"../../database/repositories/menu.js\";\nimport type { Database } from \"../../database/types.js\";\nimport { getI18nConfig } from \"../../i18n/config.js\";\nimport type { ApiResult } from \"../types.js\";\n\n// Re-export entity types so route files and tests can import them from the\n// handler module without having to know about the repository layout.\nexport type {\n\tMenu,\n\tMenuItem,\n\tMenuListItem,\n\tMenuTranslation,\n\tMenuWithItems,\n} from \"../../database/repositories/menu.js\";\n\nexport interface MenuTranslationsResponse {\n\ttranslationGroup: string | null;\n\ttranslations: Array<{\n\t\tid: string;\n\t\tname: string;\n\t\tlocale: string;\n\t\tlabel: string;\n\t\tupdatedAt: string;\n\t}>;\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Error returned when a menu lookup by `name` matches multiple locale\n * variants and the caller did not pass `locale` to disambiguate. Maps to\n * HTTP 400 via `mapErrorStatus`. The available locales are surfaced in the\n * message so MCP/REST callers can recover by re-issuing with `locale`.\n */\nfunction ambiguousMenuLocaleError(\n\tname: string,\n\tlocales: readonly string[],\n): { success: false; error: { code: \"AMBIGUOUS_LOCALE\"; message: string } } {\n\tconst sortedLocales = locales.toSorted();\n\treturn {\n\t\tsuccess: false,\n\t\terror: {\n\t\t\tcode: \"AMBIGUOUS_LOCALE\",\n\t\t\tmessage: `Menu '${name}' exists in multiple locales (${sortedLocales.join(\n\t\t\t\t\", \",\n\t\t\t)}); pass 'locale' to disambiguate.`,\n\t\t},\n\t};\n}\n\ntype ResolveMenuResult =\n\t| { success: true; menu: Menu }\n\t| { success: false; error: { code: \"NOT_FOUND\" | \"AMBIGUOUS_LOCALE\"; message: string } };\n\n/**\n * Resolve a menu by name + optional locale to a single Menu, surfacing the\n * canonical NOT_FOUND / AMBIGUOUS_LOCALE errors. Every item handler relies on\n * this to translate (name, locale) into an unambiguous menu row.\n */\nasync function resolveMenu(\n\trepo: MenuRepository,\n\tname: string,\n\toptions: { locale?: string },\n): Promise<ResolveMenuResult> {\n\tconst matches = await repo.findByName(name, options);\n\tif (matches.length === 0) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\tmessage: `Menu '${name}' not found${options.locale ? ` in locale '${options.locale}'` : \"\"}`,\n\t\t\t},\n\t\t};\n\t}\n\tif (matches.length > 1) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: ambiguousMenuLocaleError(\n\t\t\t\tname,\n\t\t\t\tmatches.map((m) => m.locale),\n\t\t\t).error,\n\t\t};\n\t}\n\treturn { success: true, menu: matches[0] };\n}\n\n// ---------------------------------------------------------------------------\n// Menu handlers\n// ---------------------------------------------------------------------------\n\n/**\n * List menus with item counts. Filter by `locale` when provided.\n */\nexport async function handleMenuList(\n\tdb: Kysely<Database>,\n\toptions: { locale?: string } = {},\n): Promise<ApiResult<MenuListItem[]>> {\n\ttry {\n\t\tconst repo = new MenuRepository(db);\n\t\tconst items = await repo.findMany(options);\n\t\treturn { success: true, data: items };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"MENU_LIST_ERROR\", message: \"Failed to fetch menus\" },\n\t\t};\n\t}\n}\n\n/**\n * Create a new menu. When `translationOf` is supplied the new menu joins the\n * source menu's translation_group (and gets the source's items cloned by the\n * repository).\n */\nexport async function handleMenuCreate(\n\tdb: Kysely<Database>,\n\tinput: { name: string; label: string; locale?: string; translationOf?: string },\n): Promise<ApiResult<Menu>> {\n\ttry {\n\t\t// Translating from a source menu only makes sense when the caller\n\t\t// names the target locale: otherwise we'd silently clone into the\n\t\t// configured default, which is almost never what's intended (and\n\t\t// will collide if the source is already the default-locale menu).\n\t\t// Enforced here so REST/SDK callers get the same guard as MCP.\n\t\tif (input.translationOf && !input.locale) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"VALIDATION_ERROR\",\n\t\t\t\t\tmessage: \"`locale` is required when `translationOf` is provided\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst repo = new MenuRepository(db);\n\n\t\t// Existence check up front so the repo's \"Source not found\" throw\n\t\t// becomes a clean NOT_FOUND on the API.\n\t\tif (input.translationOf) {\n\t\t\tconst source = await repo.findById(input.translationOf);\n\t\t\tif (!source) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: { code: \"NOT_FOUND\", message: \"Source menu for translation not found\" },\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\t// Duplicate guard: same (name, locale). Falls back to the configured\n\t\t// defaultLocale to match the column DEFAULT set by migration 036.\n\t\tconst effectiveLocale = input.locale ?? getI18nConfig()?.defaultLocale ?? \"en\";\n\t\tif (await repo.existsByNameAndLocale(input.name, effectiveLocale)) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"CONFLICT\",\n\t\t\t\t\tmessage: `Menu \"${input.name}\" already exists${\n\t\t\t\t\t\tinput.locale ? ` in locale \"${input.locale}\"` : \"\"\n\t\t\t\t\t}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst menu = await repo.create(input);\n\t\treturn { success: true, data: menu };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"MENU_CREATE_ERROR\", message: \"Failed to create menu\" },\n\t\t};\n\t}\n}\n\n/**\n * Get a single menu by name. Honours an optional `locale` filter; when two\n * menus share a name across locales, the locale distinguishes them.\n *\n * Historical behaviour: when `locale` is omitted, returns the lowest-locale\n * match (deterministic). Mirrors the pre-repo handler.\n */\nexport async function handleMenuGet(\n\tdb: Kysely<Database>,\n\tname: string,\n\toptions: { locale?: string } = {},\n): Promise<ApiResult<MenuWithItems>> {\n\ttry {\n\t\tconst repo = new MenuRepository(db);\n\t\tconst matches = await repo.findByName(name, options);\n\t\tif (matches.length === 0) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: `Menu '${name}' not found` },\n\t\t\t};\n\t\t}\n\t\tconst menu = matches[0];\n\t\tconst items = await repo.findItems(menu.id);\n\t\treturn { success: true, data: { ...menu, items } };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"MENU_GET_ERROR\", message: \"Failed to fetch menu\" },\n\t\t};\n\t}\n}\n\n/**\n * Get a menu by id. Useful when the caller already has the id (e.g. after\n * creating a translation and navigating to it).\n */\nexport async function handleMenuGetById(\n\tdb: Kysely<Database>,\n\tid: string,\n): Promise<ApiResult<MenuWithItems>> {\n\ttry {\n\t\tconst repo = new MenuRepository(db);\n\t\tconst menu = await repo.findWithItems(id);\n\t\tif (!menu) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: `Menu '${id}' not found` },\n\t\t\t};\n\t\t}\n\t\treturn { success: true, data: menu };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"MENU_GET_ERROR\", message: \"Failed to fetch menu\" },\n\t\t};\n\t}\n}\n\n/**\n * Update a menu's label. The name + locale are immutable.\n */\nexport async function handleMenuUpdate(\n\tdb: Kysely<Database>,\n\tname: string,\n\tinput: { label?: string; locale?: string },\n): Promise<ApiResult<Menu>> {\n\ttry {\n\t\tconst repo = new MenuRepository(db);\n\t\tconst resolved = await resolveMenu(repo, name, { locale: input.locale });\n\t\tif (!resolved.success) return resolved;\n\t\tconst updated = await repo.update(resolved.menu.id, { label: input.label });\n\t\tif (!updated) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: `Menu '${name}' not found` },\n\t\t\t};\n\t\t}\n\t\treturn { success: true, data: updated };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"MENU_UPDATE_ERROR\", message: \"Failed to update menu\" },\n\t\t};\n\t}\n}\n\n/**\n * Delete a menu (and its items, via the repository's explicit cleanup).\n */\nexport async function handleMenuDelete(\n\tdb: Kysely<Database>,\n\tname: string,\n\toptions: { locale?: string } = {},\n): Promise<ApiResult<{ deleted: true }>> {\n\ttry {\n\t\tconst repo = new MenuRepository(db);\n\t\tconst resolved = await resolveMenu(repo, name, options);\n\t\tif (!resolved.success) return resolved;\n\t\tawait repo.delete(resolved.menu.id);\n\t\treturn { success: true, data: { deleted: true } };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"MENU_DELETE_ERROR\", message: \"Failed to delete menu\" },\n\t\t};\n\t}\n}\n\n/**\n * List every translation of a menu (by id or translation_group).\n */\nexport async function handleMenuTranslations(\n\tdb: Kysely<Database>,\n\tidOrGroup: string,\n): Promise<ApiResult<MenuTranslationsResponse>> {\n\ttry {\n\t\tconst repo = new MenuRepository(db);\n\t\tconst result = await repo.listTranslations(idOrGroup);\n\t\tif (!result) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: \"Menu not found\" },\n\t\t\t};\n\t\t}\n\t\treturn { success: true, data: result };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"MENU_TRANSLATIONS_ERROR\", message: \"Failed to list menu translations\" },\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Menu item handlers\n// ---------------------------------------------------------------------------\n\nexport type CreateMenuItemInput = CreateMenuItemRepoInput;\nexport type UpdateMenuItemInput = UpdateMenuItemRepoInput;\nexport type MenuSetItemsInput = SetMenuItem;\n\n/**\n * Add an item to a menu. The item inherits the menu's locale.\n */\nexport async function handleMenuItemCreate(\n\tdb: Kysely<Database>,\n\tmenuName: string,\n\tinput: CreateMenuItemInput,\n\toptions: { locale?: string } = {},\n): Promise<ApiResult<MenuItem>> {\n\ttry {\n\t\tconst repo = new MenuRepository(db);\n\t\tconst resolved = await resolveMenu(repo, menuName, options);\n\t\tif (!resolved.success) return resolved;\n\n\t\tconst item = await repo.createItem(resolved.menu.id, resolved.menu.locale, input);\n\t\treturn { success: true, data: item };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"MENU_ITEM_CREATE_ERROR\", message: \"Failed to create menu item\" },\n\t\t};\n\t}\n}\n\n/**\n * Update a menu item.\n */\nexport async function handleMenuItemUpdate(\n\tdb: Kysely<Database>,\n\tmenuName: string,\n\titemId: string,\n\tinput: UpdateMenuItemInput,\n\toptions: { locale?: string } = {},\n): Promise<ApiResult<MenuItem>> {\n\ttry {\n\t\tconst repo = new MenuRepository(db);\n\t\tconst resolved = await resolveMenu(repo, menuName, options);\n\t\tif (!resolved.success) return resolved;\n\n\t\tconst updated = await repo.updateItem(resolved.menu.id, itemId, input);\n\t\tif (!updated) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: \"Menu item not found\" },\n\t\t\t};\n\t\t}\n\t\treturn { success: true, data: updated };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"MENU_ITEM_UPDATE_ERROR\", message: \"Failed to update menu item\" },\n\t\t};\n\t}\n}\n\n/**\n * Delete a menu item.\n */\nexport async function handleMenuItemDelete(\n\tdb: Kysely<Database>,\n\tmenuName: string,\n\titemId: string,\n\toptions: { locale?: string } = {},\n): Promise<ApiResult<{ deleted: true }>> {\n\ttry {\n\t\tconst repo = new MenuRepository(db);\n\t\tconst resolved = await resolveMenu(repo, menuName, options);\n\t\tif (!resolved.success) return resolved;\n\n\t\tconst deleted = await repo.deleteItem(resolved.menu.id, itemId);\n\t\tif (!deleted) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: \"Menu item not found\" },\n\t\t\t};\n\t\t}\n\t\treturn { success: true, data: { deleted: true } };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"MENU_ITEM_DELETE_ERROR\", message: \"Failed to delete menu item\" },\n\t\t};\n\t}\n}\n\nexport interface ReorderItem {\n\tid: string;\n\tparentId: string | null;\n\tsortOrder: number;\n}\n\n// ---------------------------------------------------------------------------\n// Atomic-replace menu items (used by the MCP `menu_set_items` tool and admin)\n// ---------------------------------------------------------------------------\n\n/**\n * Replace the entire set of items for a menu in one atomic transaction.\n *\n * Existing items are deleted and the new list is inserted in the order\n * provided. `parentIndex` references resolve to actual parent IDs as the\n * insert proceeds.\n */\nexport async function handleMenuSetItems(\n\tdb: Kysely<Database>,\n\tmenuName: string,\n\titems: MenuSetItemsInput[],\n\toptions: { locale?: string } = {},\n): Promise<ApiResult<{ name: string; itemCount: number }>> {\n\t// Validate parentIndex references — must be strictly earlier so the array\n\t// can be inserted in order with parents resolved first. Negative indices\n\t// are caught by Zod's `.nonnegative()` at the MCP boundary, but we guard\n\t// explicitly so REST routes / direct handler use get the same error.\n\tfor (let i = 0; i < items.length; i++) {\n\t\tconst item = items[i];\n\t\tif (item?.parentIndex !== undefined) {\n\t\t\tif (item.parentIndex < 0 || item.parentIndex >= i) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: {\n\t\t\t\t\t\tcode: \"VALIDATION_ERROR\",\n\t\t\t\t\t\tmessage: `item[${i}].parentIndex (${item.parentIndex}) must reference an earlier item`,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t}\n\n\ttry {\n\t\tconst repo = new MenuRepository(db);\n\t\tconst resolved = await resolveMenu(repo, menuName, options);\n\t\tif (!resolved.success) return resolved;\n\n\t\tconst { itemCount } = await repo.setItems(resolved.menu.id, resolved.menu.locale, items);\n\t\treturn { success: true, data: { name: menuName, itemCount } };\n\t} catch (error) {\n\t\t// `MenuGoneError` is thrown from inside the repository transaction\n\t\t// when the menu was deleted concurrently between `resolveMenu` and the\n\t\t// setItems write. Returning NOT_FOUND mirrors the original handler's\n\t\t// in-transaction `notFoundSentinel` branch and keeps the response\n\t\t// shape stable for REST/MCP callers.\n\t\tif (error instanceof MenuGoneError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Menu '${menuName}' not found${\n\t\t\t\t\t\toptions.locale ? ` in locale '${options.locale}'` : \"\"\n\t\t\t\t\t}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"[emdash] handleMenuSetItems failed:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"MENU_SET_ITEMS_ERROR\", message: \"Failed to set menu items\" },\n\t\t};\n\t}\n}\n\n/**\n * Batch reorder menu items.\n */\nexport async function handleMenuItemReorder(\n\tdb: Kysely<Database>,\n\tmenuName: string,\n\titems: ReorderItem[],\n\toptions: { locale?: string } = {},\n): Promise<ApiResult<MenuItem[]>> {\n\ttry {\n\t\tconst repo = new MenuRepository(db);\n\t\tconst resolved = await resolveMenu(repo, menuName, options);\n\t\tif (!resolved.success) return resolved;\n\n\t\tconst updated = await repo.reorderItems(resolved.menu.id, items);\n\t\treturn { success: true, data: updated };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"MENU_REORDER_ERROR\", message: \"Failed to reorder menu items\" },\n\t\t};\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"redirects-BBq3n2Rj.mjs","names":[],"sources":["../src/api/schemas/bylines.ts","../src/api/schemas/content.ts","../src/api/schemas/schema.ts","../src/api/schemas/comments.ts","../src/utils/url.ts","../src/api/schemas/menus.ts","../src/api/schemas/taxonomies.ts","../src/api/schemas/sections.ts","../src/api/schemas/settings.ts","../src/api/schemas/search.ts","../src/api/schemas/users.ts","../src/api/schemas/widgets.ts","../src/api/schemas/redirects.ts"],"sourcesContent":["import { z } from \"zod\";\n\nimport { cursorPaginationQuery, httpUrl } from \"./common.js\";\n\n/** Slug pattern: lowercase letters, digits, and hyphens; must start with a letter */\nconst bylineSlugPattern = /^[a-z][a-z0-9-]*$/;\n\nexport const bylineSummarySchema = z\n\t.object({\n\t\tid: z.string(),\n\t\tslug: z.string(),\n\t\tdisplayName: z.string(),\n\t\tbio: z.string().nullable(),\n\t\tavatarMediaId: z.string().nullable(),\n\t\t/**\n\t\t * Avatar media storage key + alt, folded in by the media join during\n\t\t * content byline hydration. Null on the plain byline finders, which\n\t\t * don't join media.\n\t\t */\n\t\tavatarStorageKey: z.string().nullish(),\n\t\tavatarAlt: z.string().nullish(),\n\t\t/**\n\t\t * Avatar media LQIP placeholder (blurhash + dominant colour, migration\n\t\t * 024), from the same media join. Lets clients render a placeholder\n\t\t * while the avatar loads. Null under the same conditions as\n\t\t * `avatarStorageKey`.\n\t\t */\n\t\tavatarBlurhash: z.string().nullish(),\n\t\tavatarDominantColor: z.string().nullish(),\n\t\twebsiteUrl: z.string().nullable(),\n\t\tuserId: z.string().nullable(),\n\t\tisGuest: z.boolean(),\n\t\tcreatedAt: z.string(),\n\t\tupdatedAt: z.string(),\n\t\t/** Locale this byline row is presented in (migration 040). */\n\t\tlocale: z.string(),\n\t\t/**\n\t\t * Shared across translations of the same byline (migration 040).\n\t\t * Equals `id` for the anchor row; siblings inherit it from their\n\t\t * source. Nullable in storage for backwards compatibility.\n\t\t */\n\t\ttranslationGroup: z.string().nullable(),\n\t\t/**\n\t\t * Byline custom-field values (Discussion #1174). Keys are slugs\n\t\t * registered via the byline-fields admin API; values follow\n\t\t * `CustomFieldValue` (`string | boolean | null`). Always present\n\t\t * on hydrated responses — empty `{}` when no fields are\n\t\t * registered (Phase 3 AC #6). Marked optional in the schema for\n\t\t * historic-payload compatibility with pre-Phase-3 clients that\n\t\t * may not send the key on writes; hydration always populates it.\n\t\t */\n\t\tcustomFields: z.record(z.string(), z.union([z.string(), z.boolean(), z.null()])).optional(),\n\t})\n\t.meta({ id: \"BylineSummary\" });\n\nexport const bylineCreditSchema = z\n\t.object({\n\t\tbyline: bylineSummarySchema,\n\t\tsortOrder: z.number().int(),\n\t\troleLabel: z.string().nullable(),\n\t\tsource: z.enum([\"explicit\", \"inferred\"]).optional().meta({\n\t\t\tdescription: \"Whether this credit was explicitly assigned or inferred from authorId\",\n\t\t}),\n\t})\n\t.meta({ id: \"BylineCredit\" });\n\nexport const contentBylineInputSchema = z\n\t.object({\n\t\tbylineId: z.string().min(1),\n\t\troleLabel: z.string().nullish(),\n\t})\n\t.meta({ id: \"ContentBylineInput\" });\n\nexport const bylinesListQuery = cursorPaginationQuery\n\t.extend({\n\t\tsearch: z.string().optional(),\n\t\tisGuest: z.coerce.boolean().optional(),\n\t\tuserId: z.string().optional(),\n\t\t/**\n\t\t * Filter by locale (strict per-locale matching, post-migration 040).\n\t\t * Rejects empty strings so the picker can't silently fetch the\n\t\t * unfiltered list when the admin URL has `?locale=` with no value.\n\t\t */\n\t\tlocale: z.string().min(1).optional(),\n\t})\n\t.meta({ id: \"BylinesListQuery\" });\n\nexport const bylineCreateBody = z\n\t.object({\n\t\tslug: z\n\t\t\t.string()\n\t\t\t.min(1)\n\t\t\t.regex(bylineSlugPattern, \"Slug must contain only lowercase letters, digits, and hyphens\"),\n\t\tdisplayName: z.string().min(1),\n\t\tbio: z.string().nullish(),\n\t\tavatarMediaId: z.string().nullish(),\n\t\twebsiteUrl: httpUrl.nullish(),\n\t\tuserId: z.string().nullish(),\n\t\tisGuest: z.boolean().optional(),\n\t\t/**\n\t\t * Locale this byline row belongs to. When omitted, the DB DEFAULT (the\n\t\t * configured `defaultLocale`) is used. Rejects empty strings — an\n\t\t * empty locale would create rows no resolver requests.\n\t\t */\n\t\tlocale: z.string().min(1).optional(),\n\t\t/**\n\t\t * When set, the new row joins the source byline's translation_group\n\t\t * rather than minting a fresh one. Requires `locale`.\n\t\t */\n\t\ttranslationOf: z.string().min(1).optional(),\n\t\t/**\n\t\t * Byline custom-field values (Discussion #1174, Phase 6 — create-flow\n\t\t * parity with update). Keys are field slugs; values are unknown at\n\t\t * the API layer because the per-field type contract lives in the\n\t\t * registry and would require an extra query to enforce here. The\n\t\t * repository's `coerceFieldValue` validates against the field's\n\t\t * type and throws `EmDashValidationError` on mismatch — the route\n\t\t * maps that to a 400 `VALIDATION_ERROR`. Reserved-slug write\n\t\t * attempts fall out as `EmDashValidationError(\"Unknown byline\n\t\t * custom field …\")` because no registered field claims a reserved\n\t\t * slug.\n\t\t */\n\t\tcustomFields: z.record(z.string(), z.unknown()).optional(),\n\t})\n\t.meta({ id: \"BylineCreateBody\" });\n\nexport const bylineTranslationCreateBody = z\n\t.object({\n\t\tlocale: z.string().min(1),\n\t\tslug: z\n\t\t\t.string()\n\t\t\t.min(1)\n\t\t\t.regex(bylineSlugPattern, \"Slug must contain only lowercase letters, digits, and hyphens\")\n\t\t\t.optional(),\n\t\tdisplayName: z.string().min(1).optional(),\n\t\tbio: z.string().nullish(),\n\t\tavatarMediaId: z.string().nullish(),\n\t\twebsiteUrl: httpUrl.nullish(),\n\t})\n\t.meta({ id: \"BylineTranslationCreateBody\" });\n\nexport const bylineTranslationsResponseSchema = z\n\t.object({\n\t\titems: z.array(bylineSummarySchema),\n\t})\n\t.meta({ id: \"BylineTranslationsResponse\" });\n\nexport const bylineUpdateBody = z\n\t.object({\n\t\tslug: z\n\t\t\t.string()\n\t\t\t.min(1)\n\t\t\t.regex(bylineSlugPattern, \"Slug must contain only lowercase letters, digits, and hyphens\")\n\t\t\t.optional(),\n\t\tdisplayName: z.string().min(1).optional(),\n\t\tbio: z.string().nullish(),\n\t\tavatarMediaId: z.string().nullish(),\n\t\twebsiteUrl: httpUrl.nullish(),\n\t\tuserId: z.string().nullish(),\n\t\tisGuest: z.boolean().optional(),\n\t\t/**\n\t\t * Byline custom-field values (Discussion #1174, Phase 3+4). Keys\n\t\t * are field slugs; values are unknown at the API layer because\n\t\t * the per-field type contract lives in the registry and would\n\t\t * require an extra query to enforce here. The repository's\n\t\t * `coerceFieldValue` validates against the field's type and\n\t\t * throws `EmDashValidationError` on mismatch — the route maps\n\t\t * that to a 400 `VALIDATION_ERROR`. Reserved-slug write attempts\n\t\t * fall out as `EmDashValidationError(\"Unknown byline custom\n\t\t * field …\")` because no registered field claims a reserved slug.\n\t\t */\n\t\tcustomFields: z.record(z.string(), z.unknown()).optional(),\n\t})\n\t.meta({ id: \"BylineUpdateBody\" });\n\nexport const bylineListResponseSchema = z\n\t.object({\n\t\titems: z.array(bylineSummarySchema),\n\t\tnextCursor: z.string().optional(),\n\t})\n\t.meta({ id: \"BylineListResponse\" });\n","import { z } from \"zod\";\n\nimport { bylineSummarySchema, bylineCreditSchema, contentBylineInputSchema } from \"./bylines.js\";\nimport { cursorPaginationQuery, httpUrl, localeCode } from \"./common.js\";\n\n// ---------------------------------------------------------------------------\n// Content: Input schemas\n// ---------------------------------------------------------------------------\n\n/** SEO input — per-content meta fields */\nexport const contentSeoInput = z\n\t.object({\n\t\ttitle: z.string().max(200).nullish(),\n\t\tdescription: z.string().max(500).nullish(),\n\t\timage: z.string().nullish(),\n\t\tcanonical: httpUrl.nullish(),\n\t\tnoIndex: z.boolean().optional(),\n\t})\n\t.meta({ id: \"ContentSeoInput\" });\n\n/** ISO 8601 date or datetime bound for the content-list date range filter. */\nconst contentDateBound = z\n\t.union([\n\t\tz.iso.datetime({ offset: true, message: \"must be an ISO 8601 datetime\" }),\n\t\tz.iso.date({ message: \"must be an ISO 8601 date\" }),\n\t])\n\t.optional();\n\nexport const contentListQuery = cursorPaginationQuery\n\t.extend({\n\t\tstatus: z.string().optional(),\n\t\torderBy: z.string().optional(),\n\t\torder: z.enum([\"asc\", \"desc\"]).optional(),\n\t\tlocale: localeCode.optional(),\n\t\t/** Case-insensitive substring search across the collection's title/name/slug. */\n\t\tq: z.string().trim().min(1).max(200).optional(),\n\t\t/** Filter to entries authored by this user (the `author_id` column). */\n\t\tauthorId: z.string().min(1).max(64).optional(),\n\t\t/** Which timestamp column the `dateFrom`/`dateTo` range applies to. */\n\t\tdateField: z.enum([\"createdAt\", \"updatedAt\", \"publishedAt\"]).optional(),\n\t\t/** Inclusive lower bound for the date range. Requires `dateField`. */\n\t\tdateFrom: contentDateBound,\n\t\t/** Inclusive upper bound for the date range. Requires `dateField`. */\n\t\tdateTo: contentDateBound,\n\t})\n\t.meta({ id: \"ContentListQuery\" });\n\n/** ISO 8601 datetime for `publishedAt` / `createdAt`. Routes gate writes behind `content:publish_any`. */\nconst contentDateOverride = z.iso\n\t.datetime({ offset: true, message: \"must be an ISO 8601 datetime\" })\n\t.nullish();\n\nexport const contentCreateBody = z\n\t.object({\n\t\tdata: z.record(z.string(), z.unknown()),\n\t\tslug: z.string().nullish(),\n\t\tstatus: z.enum([\"draft\"]).optional(),\n\t\tbylines: z.array(contentBylineInputSchema).optional(),\n\t\tlocale: localeCode.optional(),\n\t\ttranslationOf: z.string().optional(),\n\t\tseo: contentSeoInput.optional(),\n\t\ttaxonomies: z.record(z.string(), z.array(z.string())).optional().meta({\n\t\t\tdescription:\n\t\t\t\t\"Taxonomy term assignments as { taxonomyName: [termSlug, ...] }, resolved in the entry's locale.\",\n\t\t}),\n\t\tpublishedAt: contentDateOverride,\n\t\tcreatedAt: contentDateOverride,\n\t})\n\t.meta({ id: \"ContentCreateBody\" });\n\nexport const contentUpdateBody = z\n\t.object({\n\t\tdata: z.record(z.string(), z.unknown()).optional(),\n\t\tslug: z.string().nullish(),\n\t\tstatus: z.enum([\"draft\"]).optional(),\n\t\tauthorId: z.string().nullish(),\n\t\tbylines: z.array(contentBylineInputSchema).optional(),\n\t\t_rev: z\n\t\t\t.string()\n\t\t\t.optional()\n\t\t\t.meta({ description: \"Opaque revision token for optimistic concurrency\" }),\n\t\tskipRevision: z.boolean().optional(),\n\t\tseo: contentSeoInput.optional(),\n\t\ttaxonomies: z.record(z.string(), z.array(z.string())).optional().meta({\n\t\t\tdescription:\n\t\t\t\t\"Replace taxonomy assignments as { taxonomyName: [termSlug, ...] }. Only named taxonomies are touched; pass an empty array to clear a taxonomy.\",\n\t\t}),\n\t\tpublishedAt: contentDateOverride,\n\t})\n\t.meta({ id: \"ContentUpdateBody\" });\n\nexport const contentScheduleBody = z\n\t.object({\n\t\tscheduledAt: z.string().min(1, \"scheduledAt is required\").meta({\n\t\t\tdescription: \"ISO 8601 datetime for scheduled publishing\",\n\t\t\texample: \"2025-06-15T09:00:00Z\",\n\t\t}),\n\t})\n\t.meta({ id: \"ContentScheduleBody\" });\n\nexport const contentPublishBody = z\n\t.object({\n\t\t// .optional() rather than .nullish(): publishing has no semantic\n\t\t// meaning for `null` (you can't \"clear\" a publish timestamp by\n\t\t// publishing). Tightening the schema here means callers either\n\t\t// pass a valid datetime or omit the field, and the route doesn't\n\t\t// have to silently drop a null that snuck through.\n\t\tpublishedAt: z.iso\n\t\t\t.datetime({ offset: true, message: \"must be an ISO 8601 datetime\" })\n\t\t\t.optional()\n\t\t\t.meta({\n\t\t\t\tdescription:\n\t\t\t\t\t\"Optional ISO 8601 datetime to backdate the publish (e.g. when migrating content). Requires content:publish_any permission. Without this, existing published_at is preserved on re-publish.\",\n\t\t\t}),\n\t})\n\t.meta({ id: \"ContentPublishBody\" });\n\nexport const contentPreviewUrlBody = z\n\t.object({\n\t\texpiresIn: z.union([z.string(), z.number()]).optional(),\n\t\tpathPattern: z.string().optional(),\n\t})\n\t.meta({ id: \"ContentPreviewUrlBody\" });\n\nexport const contentTermsBody = z\n\t.object({\n\t\ttermIds: z.array(z.string()),\n\t})\n\t.meta({ id: \"ContentTermsBody\" });\n\nexport const contentTrashQuery = cursorPaginationQuery;\n\n// ---------------------------------------------------------------------------\n// Content: Response schemas\n// ---------------------------------------------------------------------------\n\n/** SEO metadata on a content item */\nexport const contentSeoSchema = z\n\t.object({\n\t\ttitle: z.string().nullable(),\n\t\tdescription: z.string().nullable(),\n\t\timage: z.string().nullable(),\n\t\tcanonical: z.string().nullable(),\n\t\tnoIndex: z.boolean(),\n\t})\n\t.meta({ id: \"ContentSeo\" });\n\n/** A single content item as returned by the API */\nexport const contentItemSchema = z\n\t.object({\n\t\tid: z.string(),\n\t\ttype: z.string().meta({ description: \"Collection slug this item belongs to\" }),\n\t\tslug: z.string().nullable(),\n\t\tstatus: z.string().meta({ description: \"draft, published, or scheduled\" }),\n\t\tdata: z.record(z.string(), z.unknown()).meta({\n\t\t\tdescription: \"User-defined field values\",\n\t\t}),\n\t\tauthorId: z.string().nullable(),\n\t\tprimaryBylineId: z.string().nullable(),\n\t\tbyline: bylineSummarySchema.nullable().optional(),\n\t\tbylines: z.array(bylineCreditSchema).optional(),\n\t\tcreatedAt: z.string(),\n\t\tupdatedAt: z.string(),\n\t\tpublishedAt: z.string().nullable(),\n\t\tscheduledAt: z.string().nullable(),\n\t\tliveRevisionId: z.string().nullable(),\n\t\tdraftRevisionId: z.string().nullable(),\n\t\tversion: z.number().int(),\n\t\tlocale: z.string().nullable(),\n\t\ttranslationGroup: z.string().nullable(),\n\t\tseo: contentSeoSchema.optional(),\n\t})\n\t.meta({ id: \"ContentItem\" });\n\n/** Response for single content item endpoints (get, create, update) */\nexport const contentResponseSchema = z\n\t.object({\n\t\titem: contentItemSchema,\n\t\t_rev: z\n\t\t\t.string()\n\t\t\t.optional()\n\t\t\t.meta({ description: \"Opaque revision token for optimistic concurrency\" }),\n\t})\n\t.meta({ id: \"ContentResponse\" });\n\n/** Response for content list endpoints */\nexport const contentListResponseSchema = z\n\t.object({\n\t\titems: z.array(contentItemSchema),\n\t\tnextCursor: z.string().optional(),\n\t\ttotal: z.number().int().nonnegative().optional(),\n\t})\n\t.meta({ id: \"ContentListResponse\" });\n\n/** A distinct content author for the admin author filter */\nexport const contentAuthorSchema = z\n\t.object({\n\t\tid: z.string(),\n\t\tname: z.string().nullable(),\n\t\temail: z.string(),\n\t\tavatarUrl: z.string().nullable(),\n\t})\n\t.meta({ id: \"ContentAuthor\" });\n\n/** Response for the content authors endpoint */\nexport const contentAuthorsResponseSchema = z\n\t.object({\n\t\titems: z.array(contentAuthorSchema),\n\t})\n\t.meta({ id: \"ContentAuthorsResponse\" });\n\n/** Trashed content item */\nexport const trashedContentItemSchema = z\n\t.object({\n\t\tid: z.string(),\n\t\ttype: z.string(),\n\t\tslug: z.string().nullable(),\n\t\tstatus: z.string(),\n\t\tdata: z.record(z.string(), z.unknown()),\n\t\tauthorId: z.string().nullable(),\n\t\tcreatedAt: z.string(),\n\t\tupdatedAt: z.string(),\n\t\tpublishedAt: z.string().nullable(),\n\t\tdeletedAt: z.string(),\n\t})\n\t.meta({ id: \"TrashedContentItem\" });\n\n/** Response for trashed content list */\nexport const trashedContentListResponseSchema = z\n\t.object({\n\t\titems: z.array(trashedContentItemSchema),\n\t\tnextCursor: z.string().optional(),\n\t})\n\t.meta({ id: \"TrashedContentListResponse\" });\n\n/** Response for content compare (live vs draft) */\nexport const contentCompareResponseSchema = z\n\t.object({\n\t\thasChanges: z.boolean(),\n\t\tlive: z.record(z.string(), z.unknown()).nullable(),\n\t\tdraft: z.record(z.string(), z.unknown()).nullable(),\n\t})\n\t.meta({ id: \"ContentCompareResponse\" });\n\n/** Translation summary for a content item */\nexport const contentTranslationSchema = z.object({\n\tid: z.string(),\n\tlocale: z.string().nullable(),\n\tslug: z.string().nullable(),\n\tstatus: z.string(),\n\tupdatedAt: z.string(),\n});\n\n/** Response for content translations endpoint */\nexport const contentTranslationsResponseSchema = z\n\t.object({\n\t\ttranslationGroup: z.string(),\n\t\ttranslations: z.array(contentTranslationSchema),\n\t})\n\t.meta({ id: \"ContentTranslationsResponse\" });\n","import { z } from \"zod\";\n\nimport { slugPattern } from \"./common.js\";\n\n// ---------------------------------------------------------------------------\n// Schema (collections & fields): Input schemas\n// ---------------------------------------------------------------------------\n\nconst collectionSupportValues = z.enum([\"drafts\", \"revisions\", \"preview\", \"scheduling\", \"search\"]);\n\nconst collectionSourcePattern = /^(template:.+|import:.+|manual|discovered|seed)$/;\n\nconst fieldTypeValues = z.enum([\n\t\"string\",\n\t\"text\",\n\t\"url\",\n\t\"number\",\n\t\"integer\",\n\t\"boolean\",\n\t\"datetime\",\n\t\"select\",\n\t\"multiSelect\",\n\t\"portableText\",\n\t\"image\",\n\t\"file\",\n\t\"reference\",\n\t\"json\",\n\t\"slug\",\n\t\"repeater\",\n]);\n\nconst repeaterSubFieldSchema = z.object({\n\tslug: z.string().min(1).max(63).regex(slugPattern, \"Invalid slug format\"),\n\t// Keep in sync with REPEATER_SUB_FIELD_TYPES in schema/types.ts.\n\t// (\"url\" was already a documented sub-field type but missing here.)\n\ttype: z.enum([\n\t\t\"string\",\n\t\t\"text\",\n\t\t\"url\",\n\t\t\"number\",\n\t\t\"integer\",\n\t\t\"boolean\",\n\t\t\"datetime\",\n\t\t\"select\",\n\t\t\"image\",\n\t]),\n\tlabel: z.string().min(1),\n\trequired: z.boolean().optional(),\n\toptions: z.array(z.string()).optional(),\n});\n\nconst fieldValidation = z\n\t.object({\n\t\trequired: z.boolean().optional(),\n\t\tmin: z.number().optional(),\n\t\tmax: z.number().optional(),\n\t\tminLength: z.number().int().min(0).optional(),\n\t\tmaxLength: z.number().int().min(0).optional(),\n\t\tpattern: z.string().optional(),\n\t\toptions: z.array(z.string()).optional(),\n\t\tsubFields: z.array(repeaterSubFieldSchema).min(1).optional(),\n\t\tminItems: z.number().int().min(0).optional(),\n\t\tmaxItems: z.number().int().min(1).optional(),\n\t\tallowedMimeTypes: z\n\t\t\t.array(\n\t\t\t\tz\n\t\t\t\t\t.string()\n\t\t\t\t\t.regex(/^[a-z0-9][a-z0-9!#$&^_+\\-.]*\\/[a-z0-9!#$&^_+\\-.]*$/i, \"Invalid MIME type\"),\n\t\t\t)\n\t\t\t.min(1, \"allowedMimeTypes must not be empty — omit the field to allow all types\")\n\t\t\t.max(64, \"allowedMimeTypes may contain at most 64 entries\")\n\t\t\t.optional(),\n\t})\n\t.optional();\n\nconst fieldWidgetOptions = z.record(z.string(), z.unknown()).optional();\n\nexport const createCollectionBody = z\n\t.object({\n\t\tslug: z.string().min(1).max(63).regex(slugPattern, \"Invalid slug format\"),\n\t\tlabel: z.string().min(1),\n\t\tlabelSingular: z.string().optional(),\n\t\tdescription: z.string().optional(),\n\t\ticon: z.string().optional(),\n\t\tsupports: z.array(collectionSupportValues).optional(),\n\t\tsource: z.string().regex(collectionSourcePattern).optional(),\n\t\turlPattern: z.string().optional(),\n\t\thasSeo: z.boolean().optional(),\n\t})\n\t.meta({ id: \"CreateCollectionBody\" });\n\nexport const updateCollectionBody = z\n\t.object({\n\t\tlabel: z.string().min(1).optional(),\n\t\tlabelSingular: z.string().optional(),\n\t\tdescription: z.string().optional(),\n\t\ticon: z.string().optional(),\n\t\tsupports: z.array(collectionSupportValues).optional(),\n\t\turlPattern: z.string().nullish(),\n\t\thasSeo: z.boolean().optional(),\n\t\tcommentsEnabled: z.boolean().optional(),\n\t\tcommentsModeration: z.enum([\"all\", \"first_time\", \"none\"]).optional(),\n\t\tcommentsClosedAfterDays: z.number().int().min(0).optional(),\n\t\tcommentsAutoApproveUsers: z.boolean().optional(),\n\t})\n\t.meta({ id: \"UpdateCollectionBody\" });\n\nexport const createFieldBody = z\n\t.object({\n\t\tslug: z.string().min(1).max(63).regex(slugPattern, \"Invalid slug format\"),\n\t\tlabel: z.string().min(1),\n\t\ttype: fieldTypeValues,\n\t\trequired: z.boolean().optional(),\n\t\tunique: z.boolean().optional(),\n\t\tdefaultValue: z.unknown().optional(),\n\t\tvalidation: fieldValidation.nullable(),\n\t\twidget: z.string().optional(),\n\t\toptions: fieldWidgetOptions,\n\t\tsortOrder: z.number().int().min(0).optional(),\n\t\tsearchable: z.boolean().optional(),\n\t\ttranslatable: z.boolean().optional(),\n\t})\n\t.meta({ id: \"CreateFieldBody\" });\n\nexport const updateFieldBody = z\n\t.object({\n\t\tlabel: z.string().min(1).optional(),\n\t\ttype: fieldTypeValues.optional(),\n\t\trequired: z.boolean().optional(),\n\t\tunique: z.boolean().optional(),\n\t\tdefaultValue: z.unknown().optional(),\n\t\tvalidation: fieldValidation.nullable(),\n\t\twidget: z.string().optional(),\n\t\toptions: fieldWidgetOptions,\n\t\tsortOrder: z.number().int().min(0).optional(),\n\t\tsearchable: z.boolean().optional(),\n\t\ttranslatable: z.boolean().optional(),\n\t})\n\t.meta({ id: \"UpdateFieldBody\" });\n\nexport const fieldReorderBody = z\n\t.object({\n\t\tfieldSlugs: z.array(z.string().min(1)),\n\t})\n\t.meta({ id: \"FieldReorderBody\" });\n\nexport const orphanRegisterBody = z\n\t.object({\n\t\tlabel: z.string().optional(),\n\t\tlabelSingular: z.string().optional(),\n\t\tdescription: z.string().optional(),\n\t})\n\t.meta({ id: \"OrphanRegisterBody\" });\n\nexport const schemaExportQuery = z.object({\n\tformat: z.string().optional(),\n});\n\nexport const collectionGetQuery = z.object({\n\tincludeFields: z\n\t\t.string()\n\t\t.transform((v) => v === \"true\")\n\t\t.optional(),\n});\n\n// ---------------------------------------------------------------------------\n// Schema: Response schemas\n// ---------------------------------------------------------------------------\n\nexport const collectionSchema = z\n\t.object({\n\t\tid: z.string(),\n\t\tslug: z.string(),\n\t\tlabel: z.string(),\n\t\tlabelSingular: z.string().nullable(),\n\t\tdescription: z.string().nullable(),\n\t\ticon: z.string().nullable(),\n\t\tsupports: z.array(z.string()),\n\t\tsource: z.string().nullable(),\n\t\turlPattern: z.string().nullable(),\n\t\thasSeo: z.boolean(),\n\t\tcreatedAt: z.string(),\n\t\tupdatedAt: z.string(),\n\t})\n\t.meta({ id: \"Collection\" });\n\nexport const fieldSchema = z\n\t.object({\n\t\tid: z.string(),\n\t\tcollectionId: z.string(),\n\t\tslug: z.string(),\n\t\tlabel: z.string(),\n\t\ttype: fieldTypeValues,\n\t\trequired: z.boolean(),\n\t\tunique: z.boolean(),\n\t\tdefaultValue: z.unknown().nullable(),\n\t\tvalidation: z.record(z.string(), z.unknown()).nullable(),\n\t\twidget: z.string().nullable(),\n\t\toptions: z.record(z.string(), z.unknown()).nullable(),\n\t\tsortOrder: z.number().int(),\n\t\tsearchable: z.boolean(),\n\t\ttranslatable: z.boolean(),\n\t\tcreatedAt: z.string(),\n\t\tupdatedAt: z.string(),\n\t})\n\t.meta({ id: \"Field\" });\n\nexport const collectionResponseSchema = z\n\t.object({ item: collectionSchema })\n\t.meta({ id: \"CollectionResponse\" });\n\nexport const collectionWithFieldsResponseSchema = z\n\t.object({\n\t\titem: collectionSchema.extend({ fields: z.array(fieldSchema) }),\n\t})\n\t.meta({ id: \"CollectionWithFieldsResponse\" });\n\nexport const collectionListResponseSchema = z\n\t.object({ items: z.array(collectionSchema) })\n\t.meta({ id: \"CollectionListResponse\" });\n\nexport const fieldResponseSchema = z.object({ item: fieldSchema }).meta({ id: \"FieldResponse\" });\n\nexport const fieldListResponseSchema = z\n\t.object({ items: z.array(fieldSchema) })\n\t.meta({ id: \"FieldListResponse\" });\n\nexport const orphanedTableSchema = z\n\t.object({\n\t\tslug: z.string(),\n\t\ttableName: z.string(),\n\t\trowCount: z.number().int(),\n\t})\n\t.meta({ id: \"OrphanedTable\" });\n\nexport const orphanedTableListResponseSchema = z\n\t.object({ items: z.array(orphanedTableSchema) })\n\t.meta({ id: \"OrphanedTableListResponse\" });\n","import { z } from \"zod\";\n\n// ---------------------------------------------------------------------------\n// Comments: Input schemas\n// ---------------------------------------------------------------------------\n\nexport const createCommentBody = z\n\t.object({\n\t\tauthorName: z.string().min(1).max(100),\n\t\tauthorEmail: z.string().email(),\n\t\tbody: z.string().min(1).max(5000),\n\t\tparentId: z.string().optional(),\n\t\t/** Honeypot field — hidden in the form, filled only by bots */\n\t\twebsite_url: z.string().optional(),\n\t\t/**\n\t\t * Turnstile response token from the form widget. Verified\n\t\t * server-side when a Turnstile secret key is configured.\n\t\t * Turnstile tokens can be up to 2048 characters.\n\t\t */\n\t\tturnstileToken: z.string().max(2048).optional(),\n\t})\n\t.meta({ id: \"CreateCommentBody\" });\n\nexport const createReactionBody = z\n\t.object({\n\t\tcommentId: z.string().min(1),\n\t\t/** Reaction name. Positive-only (\"like\") by default; extensible. */\n\t\treaction: z.string().min(1).max(20).default(\"like\"),\n\t\t/** Honeypot field — hidden in the form, filled only by bots */\n\t\twebsite_url: z.string().optional(),\n\t})\n\t.meta({ id: \"CreateReactionBody\" });\n\nexport const commentStatusBody = z\n\t.object({\n\t\tstatus: z.enum([\"approved\", \"pending\", \"spam\", \"trash\"]),\n\t})\n\t.meta({ id: \"CommentStatusBody\" });\n\nexport const commentBulkBody = z\n\t.object({\n\t\tids: z.array(z.string().min(1)).min(1).max(100),\n\t\taction: z.enum([\"approve\", \"spam\", \"trash\", \"delete\"]),\n\t})\n\t.meta({ id: \"CommentBulkBody\" });\n\nexport const commentListQuery = z\n\t.object({\n\t\tstatus: z.enum([\"pending\", \"approved\", \"spam\", \"trash\"]).optional(),\n\t\tcollection: z.string().optional(),\n\t\tsearch: z.string().optional(),\n\t\tlimit: z.coerce.number().int().min(1).max(100).optional().default(50),\n\t\tcursor: z.string().max(2048).optional(),\n\t})\n\t.meta({ id: \"CommentListQuery\" });\n\n// ---------------------------------------------------------------------------\n// Comments: Response schemas\n// ---------------------------------------------------------------------------\n\nconst commentStatusValues = z.enum([\"pending\", \"approved\", \"spam\", \"trash\"]);\n\n/**\n * Public-facing comment (no email/IP).\n *\n * `replies` is recursive in practice (each reply can have replies), but we\n * model it as a single level here to avoid circular type inference issues\n * with tsgo. OpenAPI consumers should treat replies as the same shape.\n */\nexport const publicCommentSchema: z.ZodObject<{\n\tid: z.ZodString;\n\tauthorName: z.ZodString;\n\tisRegisteredUser: z.ZodBoolean;\n\tbody: z.ZodString;\n\tparentId: z.ZodNullable<z.ZodString>;\n\tcreatedAt: z.ZodString;\n\treplies: z.ZodOptional<z.ZodArray<z.ZodAny>>;\n}> = z\n\t.object({\n\t\tid: z.string(),\n\t\tauthorName: z.string(),\n\t\tisRegisteredUser: z.boolean(),\n\t\tbody: z.string(),\n\t\tparentId: z.string().nullable(),\n\t\tcreatedAt: z.string(),\n\t\treplies: z.array(z.any()).optional(),\n\t})\n\t.meta({ id: \"PublicComment\" });\n\n/** Admin comment with full details */\nexport const commentSchema = z\n\t.object({\n\t\tid: z.string(),\n\t\tcollection: z.string(),\n\t\tcontentId: z.string(),\n\t\tauthorName: z.string(),\n\t\tauthorEmail: z.string(),\n\t\tbody: z.string(),\n\t\tstatus: commentStatusValues,\n\t\tparentId: z.string().nullable(),\n\t\tipHash: z.string().nullable(),\n\t\tcreatedAt: z.string(),\n\t\tupdatedAt: z.string(),\n\t})\n\t.meta({ id: \"Comment\" });\n\nexport const publicCommentListResponseSchema = z\n\t.object({\n\t\titems: z.array(publicCommentSchema),\n\t\tnextCursor: z.string().optional(),\n\t\ttotal: z.number().int(),\n\t})\n\t.meta({ id: \"PublicCommentListResponse\" });\n\nexport const adminCommentListResponseSchema = z\n\t.object({\n\t\titems: z.array(commentSchema),\n\t\tnextCursor: z.string().optional(),\n\t})\n\t.meta({ id: \"AdminCommentListResponse\" });\n\nexport const commentCountsResponseSchema = z\n\t.object({\n\t\tpending: z.number().int(),\n\t\tapproved: z.number().int(),\n\t\tspam: z.number().int(),\n\t\ttrash: z.number().int(),\n\t})\n\t.meta({ id: \"CommentCountsResponse\" });\n\nexport const commentBulkResponseSchema = z\n\t.object({ affected: z.number().int() })\n\t.meta({ id: \"CommentBulkResponse\" });\n","/**\n * URL scheme validation utilities\n *\n * Prevents XSS via dangerous URL schemes (javascript:, data:, vbscript:, etc.)\n * by allowlisting known-safe schemes before rendering into href attributes.\n */\n\n/**\n * Matches URLs that are safe to render in href attributes.\n *\n * Allowed:\n * - http:// and https://\n * - mailto: and tel:\n * - Relative paths (starting with /)\n * - Fragment links (starting with #)\n * - Protocol-relative URLs are NOT allowed (starting with //) as they can\n * redirect to attacker-controlled hosts.\n */\nconst SAFE_URL_SCHEME_RE = /^(https?:|mailto:|tel:|\\/(?!\\/)|#)/i;\n\n/**\n * Returns the URL unchanged if it uses a safe scheme, otherwise returns \"#\".\n *\n * Use this at the render layer as the primary defense against XSS via\n * dangerous URL schemes like `javascript:`, `data:`, or `vbscript:`.\n *\n * @example\n * ```ts\n * sanitizeHref(\"https://example.com\") // \"https://example.com\"\n * sanitizeHref(\"/about\") // \"/about\"\n * sanitizeHref(\"#section\") // \"#section\"\n * sanitizeHref(\"mailto:a@b.com\") // \"mailto:a@b.com\"\n * sanitizeHref(\"javascript:alert(1)\") // \"#\"\n * sanitizeHref(\"data:text/html,<script>\") // \"#\"\n * sanitizeHref(\"\") // \"#\"\n * ```\n */\nexport function sanitizeHref(url: string | undefined | null): string {\n\tif (!url) return \"#\";\n\treturn SAFE_URL_SCHEME_RE.test(url) ? url : \"#\";\n}\n\n/**\n * Returns true if the URL uses a safe scheme for rendering in href attributes.\n */\nexport function isSafeHref(url: string): boolean {\n\treturn SAFE_URL_SCHEME_RE.test(url);\n}\n","import { z } from \"zod\";\n\nimport { isSafeHref } from \"../../utils/url.js\";\n\n// ---------------------------------------------------------------------------\n// Menus: Input schemas\n// ---------------------------------------------------------------------------\n\n/**\n * Allowed menu item types. `custom` uses `customUrl`; the others resolve a URL\n * from `referenceCollection` + `referenceId` (a translation_group id).\n */\nexport const menuItemTypeEnum = z.enum([\"custom\", \"page\", \"post\", \"taxonomy\", \"collection\"]);\n\nconst safeHref = z\n\t.string()\n\t.trim()\n\t.refine(\n\t\tisSafeHref,\n\t\t\"URL must use http, https, mailto, tel, a relative path, or a fragment identifier\",\n\t);\n\nexport const createMenuBody = z\n\t.object({\n\t\tname: z.string().min(1),\n\t\tlabel: z.string().min(1),\n\t\tlocale: z.string().min(1).optional(),\n\t\t/** When set, clones the items from the source menu. The new menu joins\n\t\t * the source's translation_group. */\n\t\ttranslationOf: z.string().min(1).optional(),\n\t})\n\t.strict()\n\t.meta({ id: \"CreateMenuBody\" });\n\nexport const updateMenuBody = z\n\t.object({\n\t\tlabel: z.string().min(1).optional(),\n\t})\n\t.strict()\n\t.meta({ id: \"UpdateMenuBody\" });\n\nexport const createMenuItemBody = z\n\t.object({\n\t\ttype: menuItemTypeEnum,\n\t\tlabel: z.string().min(1),\n\t\treferenceCollection: z.string().optional(),\n\t\treferenceId: z.string().optional(),\n\t\tcustomUrl: safeHref.optional(),\n\t\ttarget: z.string().optional(),\n\t\ttitleAttr: z.string().optional(),\n\t\tcssClasses: z.string().optional(),\n\t\tparentId: z.string().optional(),\n\t\tsortOrder: z.number().int().min(0).optional(),\n\t})\n\t.strict()\n\t.meta({ id: \"CreateMenuItemBody\" });\n\nexport const updateMenuItemBody = z\n\t.object({\n\t\tlabel: z.string().min(1).optional(),\n\t\tcustomUrl: safeHref.optional(),\n\t\ttarget: z.string().optional(),\n\t\ttitleAttr: z.string().optional(),\n\t\tcssClasses: z.string().optional(),\n\t\tparentId: z.string().nullish(),\n\t\tsortOrder: z.number().int().min(0).optional(),\n\t})\n\t.strict()\n\t.meta({ id: \"UpdateMenuItemBody\" });\n\nexport const reorderMenuItemsBody = z\n\t.object({\n\t\titems: z.array(\n\t\t\tz.object({\n\t\t\t\tid: z.string().min(1),\n\t\t\t\tparentId: z.string().nullable(),\n\t\t\t\tsortOrder: z.number().int().min(0),\n\t\t\t}),\n\t\t),\n\t})\n\t.meta({ id: \"ReorderMenuItemsBody\" });\n\n// ---------------------------------------------------------------------------\n// Menus: Response schemas\n//\n// All responses are camelCase to align with the rest of the EmDash REST API\n// (content, taxonomies, redirects, etc.). The DB columns are snake_case;\n// handlers hydrate rows into the shapes below before returning.\n// ---------------------------------------------------------------------------\n\nexport const menuSchema = z\n\t.object({\n\t\tid: z.string(),\n\t\tname: z.string(),\n\t\tlabel: z.string(),\n\t\tcreatedAt: z.string(),\n\t\tupdatedAt: z.string(),\n\t\tlocale: z.string(),\n\t\ttranslationGroup: z.string().nullable(),\n\t})\n\t.meta({ id: \"Menu\" });\n\nexport const menuItemSchema = z\n\t.object({\n\t\tid: z.string(),\n\t\tmenuId: z.string(),\n\t\tparentId: z.string().nullable(),\n\t\tsortOrder: z.number().int(),\n\t\ttype: z.string(),\n\t\treferenceCollection: z.string().nullable(),\n\t\treferenceId: z.string().nullable(),\n\t\tcustomUrl: z.string().nullable(),\n\t\tlabel: z.string(),\n\t\ttitleAttr: z.string().nullable(),\n\t\ttarget: z.string().nullable(),\n\t\tcssClasses: z.string().nullable(),\n\t\tcreatedAt: z.string(),\n\t\tlocale: z.string(),\n\t\ttranslationGroup: z.string().nullable(),\n\t})\n\t.meta({ id: \"MenuItem\" });\n\nexport const menuTranslationsSchema = z\n\t.object({\n\t\ttranslationGroup: z.string().nullable(),\n\t\ttranslations: z.array(\n\t\t\tz.object({\n\t\t\t\tid: z.string(),\n\t\t\t\tname: z.string(),\n\t\t\t\tlabel: z.string(),\n\t\t\t\tlocale: z.string(),\n\t\t\t\tupdatedAt: z.string(),\n\t\t\t}),\n\t\t),\n\t})\n\t.meta({ id: \"MenuTranslations\" });\n\nexport const menuListItemSchema = menuSchema\n\t.extend({\n\t\titemCount: z.number().int(),\n\t})\n\t.meta({ id: \"MenuListItem\" });\n\nexport const menuWithItemsSchema = menuSchema\n\t.extend({\n\t\titems: z.array(menuItemSchema),\n\t})\n\t.meta({ id: \"MenuWithItems\" });\n","import { z } from \"zod\";\n\n// ---------------------------------------------------------------------------\n// Taxonomy definitions: Input schemas\n// ---------------------------------------------------------------------------\n\n/** Collection slug format: lowercase alphanumeric + underscores, starts with letter */\nconst collectionSlugPattern = /^[a-z][a-z0-9_]*$/;\n\nexport const createTaxonomyDefBody = z\n\t.object({\n\t\tname: z\n\t\t\t.string()\n\t\t\t.min(1)\n\t\t\t.max(63)\n\t\t\t.regex(/^[a-z][a-z0-9_]*$/, \"Name must be lowercase alphanumeric with underscores\"),\n\t\tlabel: z.string().min(1).max(200),\n\t\tlabelSingular: z.string().min(1).max(200).optional(),\n\t\thierarchical: z.boolean().optional().default(false),\n\t\tcollections: z\n\t\t\t.array(\n\t\t\t\tz.string().min(1).max(63).regex(collectionSlugPattern, \"Invalid collection slug format\"),\n\t\t\t)\n\t\t\t.max(100)\n\t\t\t.optional()\n\t\t\t.default([]),\n\t\tlocale: z.string().min(1).optional(),\n\t\ttranslationOf: z.string().min(1).optional(),\n\t})\n\t.meta({ id: \"CreateTaxonomyDefBody\" });\n\n// ---------------------------------------------------------------------------\n// Taxonomy terms: Input schemas\n// ---------------------------------------------------------------------------\n\nexport const createTermBody = z\n\t.object({\n\t\tslug: z.string().min(1),\n\t\tlabel: z.string().min(1),\n\t\tparentId: z.string().nullish(),\n\t\tdescription: z.string().optional(),\n\t\tlocale: z.string().min(1).optional(),\n\t\ttranslationOf: z.string().min(1).optional(),\n\t})\n\t.meta({ id: \"CreateTermBody\" });\n\nexport const updateTermBody = z\n\t.object({\n\t\tslug: z.string().min(1).optional(),\n\t\tlabel: z.string().min(1).optional(),\n\t\tparentId: z.string().nullish(),\n\t\tdescription: z.string().optional(),\n\t})\n\t.meta({ id: \"UpdateTermBody\" });\n\n// ---------------------------------------------------------------------------\n// Taxonomies: Response schemas\n// ---------------------------------------------------------------------------\n\nexport const taxonomyDefSchema = z\n\t.object({\n\t\tid: z.string(),\n\t\tname: z.string(),\n\t\tlabel: z.string(),\n\t\tlabelSingular: z.string().optional(),\n\t\thierarchical: z.boolean(),\n\t\tcollections: z.array(z.string()),\n\t\tlocale: z.string(),\n\t\ttranslationGroup: z.string().nullable(),\n\t})\n\t.meta({ id: \"TaxonomyDef\" });\n\nexport const taxonomyDefTranslationsSchema = z\n\t.object({\n\t\ttranslationGroup: z.string().nullable(),\n\t\ttranslations: z.array(\n\t\t\tz.object({\n\t\t\t\tid: z.string(),\n\t\t\t\tname: z.string(),\n\t\t\t\tlabel: z.string(),\n\t\t\t\tlocale: z.string(),\n\t\t\t}),\n\t\t),\n\t})\n\t.meta({ id: \"TaxonomyDefTranslations\" });\n\nexport const taxonomyListResponseSchema = z\n\t.object({ taxonomies: z.array(taxonomyDefSchema) })\n\t.meta({ id: \"TaxonomyListResponse\" });\n\nexport const termSchema = z\n\t.object({\n\t\tid: z.string(),\n\t\tname: z.string(),\n\t\tslug: z.string(),\n\t\tlabel: z.string(),\n\t\tparentId: z.string().nullable(),\n\t\tdescription: z.string().optional(),\n\t\tlocale: z.string(),\n\t\ttranslationGroup: z.string().nullable(),\n\t})\n\t.meta({ id: \"Term\" });\n\nexport const termTranslationsSchema = z\n\t.object({\n\t\ttranslationGroup: z.string().nullable(),\n\t\ttranslations: z.array(\n\t\t\tz.object({\n\t\t\t\tid: z.string(),\n\t\t\t\tslug: z.string(),\n\t\t\t\tlabel: z.string(),\n\t\t\t\tlocale: z.string(),\n\t\t\t}),\n\t\t),\n\t})\n\t.meta({ id: \"TermTranslations\" });\n\nexport const termWithCountSchema: z.ZodType = z\n\t.object({\n\t\tid: z.string(),\n\t\tname: z.string(),\n\t\tslug: z.string(),\n\t\tlabel: z.string(),\n\t\tparentId: z.string().nullable(),\n\t\tdescription: z.string().optional(),\n\t\tcount: z.number().int(),\n\t\tchildren: z.array(z.lazy(() => termWithCountSchema)),\n\t\tlocale: z.string(),\n\t\ttranslationGroup: z.string().nullable(),\n\t})\n\t.meta({ id: \"TermWithCount\" });\n\nexport const termListResponseSchema = z\n\t.object({ terms: z.array(termWithCountSchema) })\n\t.meta({ id: \"TermListResponse\" });\n\nexport const termResponseSchema = z.object({ term: termSchema }).meta({ id: \"TermResponse\" });\n\nexport const termGetResponseSchema = z\n\t.object({\n\t\tterm: termSchema.extend({\n\t\t\tcount: z.number().int(),\n\t\t\tchildren: z.array(\n\t\t\t\tz.object({\n\t\t\t\t\tid: z.string(),\n\t\t\t\t\tslug: z.string(),\n\t\t\t\t\tlabel: z.string(),\n\t\t\t\t}),\n\t\t\t),\n\t\t}),\n\t})\n\t.meta({ id: \"TermGetResponse\" });\n","import { z } from \"zod\";\n\n// ---------------------------------------------------------------------------\n// Sections: Input schemas\n// ---------------------------------------------------------------------------\n\nconst sectionSource = z.enum([\"theme\", \"user\", \"import\"]);\n\nexport const sectionsListQuery = z\n\t.object({\n\t\tsource: sectionSource.optional(),\n\t\tsearch: z.string().optional(),\n\t\tlimit: z.coerce.number().int().min(1).max(100).optional().default(50),\n\t\tcursor: z.string().max(2048).optional(),\n\t})\n\t.meta({ id: \"SectionsListQuery\" });\n\nexport const createSectionBody = z\n\t.object({\n\t\tslug: z.string().min(1),\n\t\ttitle: z.string().min(1),\n\t\tdescription: z.string().optional(),\n\t\tkeywords: z.array(z.string()).optional(),\n\t\tcontent: z.array(z.record(z.string(), z.unknown())),\n\t\tpreviewMediaId: z.string().optional(),\n\t\tsource: z.enum([\"user\", \"import\"]).optional(),\n\t\tthemeId: z.string().optional(),\n\t})\n\t.meta({ id: \"CreateSectionBody\" });\n\nexport const updateSectionBody = z\n\t.object({\n\t\tslug: z.string().min(1).optional(),\n\t\ttitle: z.string().min(1).optional(),\n\t\tdescription: z.string().optional(),\n\t\tkeywords: z.array(z.string()).optional(),\n\t\tcontent: z.array(z.record(z.string(), z.unknown())).optional(),\n\t\tpreviewMediaId: z.string().nullish(),\n\t})\n\t.meta({ id: \"UpdateSectionBody\" });\n\n// ---------------------------------------------------------------------------\n// Sections: Response schemas\n// ---------------------------------------------------------------------------\n\nexport const sectionSchema = z\n\t.object({\n\t\tid: z.string(),\n\t\tslug: z.string(),\n\t\ttitle: z.string(),\n\t\tdescription: z.string().nullable(),\n\t\tkeywords: z.array(z.string()).nullable(),\n\t\tcontent: z.array(z.record(z.string(), z.unknown())),\n\t\tpreviewMediaId: z.string().nullable(),\n\t\tsource: z.string(),\n\t\tthemeId: z.string().nullable(),\n\t\tcreatedAt: z.string(),\n\t\tupdatedAt: z.string(),\n\t})\n\t.meta({ id: \"Section\" });\n\nexport const sectionListResponseSchema = z\n\t.object({\n\t\titems: z.array(sectionSchema),\n\t\tnextCursor: z.string().optional(),\n\t})\n\t.meta({ id: \"SectionListResponse\" });\n","import { z } from \"zod\";\n\nimport { httpUrl } from \"./common.js\";\n\n// ---------------------------------------------------------------------------\n// Settings: Input schemas\n//\n// Media references on write are just `{ mediaId, alt? }` -- the resolved\n// fields (`url`, `contentType`, `width`, `height`) are server-computed and\n// stripped from any submitted body via Zod's default strip mode. See\n// `packages/core/src/settings/types.ts` for the in-memory shape.\n// ---------------------------------------------------------------------------\n\nconst mediaReferenceInput = z.object({\n\tmediaId: z.string(),\n\talt: z.string().optional(),\n});\n\nconst socialSettings = z.object({\n\ttwitter: z.string().optional(),\n\tgithub: z.string().optional(),\n\tfacebook: z.string().optional(),\n\tinstagram: z.string().optional(),\n\tlinkedin: z.string().optional(),\n\tyoutube: z.string().optional(),\n});\n\nconst seoSettingsInput = z.object({\n\ttitleSeparator: z.string().max(10).optional(),\n\tdefaultOgImage: mediaReferenceInput.optional(),\n\trobotsTxt: z.string().max(5000).optional(),\n\tgoogleVerification: z.string().max(100).optional(),\n\tbingVerification: z.string().max(100).optional(),\n});\n\nexport const settingsUpdateBody = z\n\t.object({\n\t\ttitle: z.string().optional(),\n\t\ttagline: z.string().optional(),\n\t\tlogo: mediaReferenceInput.optional(),\n\t\tfavicon: mediaReferenceInput.optional(),\n\t\turl: z.union([httpUrl, z.literal(\"\")]).optional(),\n\t\tpostsPerPage: z.number().int().min(1).max(100).optional(),\n\t\tdateFormat: z.string().optional(),\n\t\ttimezone: z.string().optional(),\n\t\tsocial: socialSettings.optional(),\n\t\tseo: seoSettingsInput.optional(),\n\t})\n\t.meta({ id: \"SettingsUpdateBody\" });\n\n// ---------------------------------------------------------------------------\n// Settings: Response schemas\n//\n// Responses carry the resolved fields populated by `resolveMediaReference`\n// in `settings/index.ts`. Generated OpenAPI clients need to see them so\n// they don't have to re-resolve the URL on the client. Fields stay\n// optional because the resolver returns the bare ref if the underlying\n// media row was deleted (orphaned reference).\n// ---------------------------------------------------------------------------\n\nconst mediaReferenceResponse = z.object({\n\tmediaId: z.string(),\n\talt: z.string().optional(),\n\t/** Resolved media file URL; absent if the underlying row is missing. */\n\turl: z.string().optional(),\n\t/** Stored MIME type (e.g. `image/svg+xml`). Populated alongside `url`. */\n\tcontentType: z.string().optional(),\n\t/** Pixel width if known. Populated alongside `url`. */\n\twidth: z.number().int().optional(),\n\t/** Pixel height if known. Populated alongside `url`. */\n\theight: z.number().int().optional(),\n});\n\nconst seoSettingsResponse = z.object({\n\ttitleSeparator: z.string().max(10).optional(),\n\tdefaultOgImage: mediaReferenceResponse.optional(),\n\trobotsTxt: z.string().max(5000).optional(),\n\tgoogleVerification: z.string().max(100).optional(),\n\tbingVerification: z.string().max(100).optional(),\n});\n\nexport const siteSettingsSchema = z\n\t.object({\n\t\ttitle: z.string().optional(),\n\t\ttagline: z.string().optional(),\n\t\tlogo: mediaReferenceResponse.optional(),\n\t\tfavicon: mediaReferenceResponse.optional(),\n\t\turl: z.string().optional(),\n\t\tpostsPerPage: z.number().int().optional(),\n\t\tdateFormat: z.string().optional(),\n\t\ttimezone: z.string().optional(),\n\t\tsocial: socialSettings.optional(),\n\t\tseo: seoSettingsResponse.optional(),\n\t})\n\t.meta({ id: \"SiteSettings\" });\n","import { z } from \"zod\";\n\nimport { localeCode } from \"./common.js\";\n\n// ---------------------------------------------------------------------------\n// Search: Input schemas\n// ---------------------------------------------------------------------------\n\nexport const searchQuery = z\n\t.object({\n\t\tq: z.string().min(1),\n\t\tcollections: z.string().optional(),\n\t\tstatus: z.string().optional(),\n\t\tlocale: localeCode.optional(),\n\t\tlimit: z.coerce.number().int().min(1).max(100).optional(),\n\t\tcursor: z.string().optional(),\n\t})\n\t.meta({ id: \"SearchQuery\" });\n\nexport const searchSuggestQuery = z\n\t.object({\n\t\tq: z.string().min(1),\n\t\tcollections: z.string().optional(),\n\t\tlocale: localeCode.optional(),\n\t\tlimit: z.coerce.number().int().min(1).max(20).optional(),\n\t})\n\t.meta({ id: \"SearchSuggestQuery\" });\n\nexport const searchRebuildBody = z\n\t.object({\n\t\tcollection: z.string().min(1),\n\t})\n\t.meta({ id: \"SearchRebuildBody\" });\n\nexport const searchEnableBody = z\n\t.object({\n\t\tcollection: z.string().min(1),\n\t\tenabled: z.boolean(),\n\t\tweights: z.record(z.string(), z.number()).optional(),\n\t})\n\t.meta({ id: \"SearchEnableBody\" });\n\n// ---------------------------------------------------------------------------\n// Search: Response schemas\n// ---------------------------------------------------------------------------\n\nexport const searchResultSchema = z\n\t.object({\n\t\tcollection: z.string(),\n\t\tid: z.string(),\n\t\tslug: z.string().nullable(),\n\t\tlocale: z.string(),\n\t\ttitle: z.string().optional(),\n\t\tsnippet: z.string().optional(),\n\t\tscore: z.number(),\n\t})\n\t.meta({ id: \"SearchResult\" });\n\nexport const searchResponseSchema = z\n\t.object({\n\t\titems: z.array(searchResultSchema),\n\t\tnextCursor: z.string().optional(),\n\t})\n\t.meta({ id: \"SearchResponse\" });\n","import { z } from \"zod\";\n\nimport { roleLevel } from \"./common.js\";\n\n// ---------------------------------------------------------------------------\n// Admin / Users: Input schemas\n// ---------------------------------------------------------------------------\n\nexport const usersListQuery = z\n\t.object({\n\t\tsearch: z.string().optional(),\n\t\trole: z.string().optional(),\n\t\tcursor: z.string().max(2048).optional(),\n\t\tlimit: z.coerce.number().int().min(1).max(100).optional().default(50),\n\t})\n\t.meta({ id: \"UsersListQuery\" });\n\nexport const userUpdateBody = z\n\t.object({\n\t\tname: z.string().optional(),\n\t\temail: z.string().email().optional(),\n\t\trole: roleLevel.optional(),\n\t})\n\t.meta({ id: \"UserUpdateBody\" });\n\nexport const allowedDomainCreateBody = z\n\t.object({\n\t\tdomain: z.string().min(1),\n\t\tdefaultRole: roleLevel,\n\t})\n\t.meta({ id: \"AllowedDomainCreateBody\" });\n\nexport const allowedDomainUpdateBody = z\n\t.object({\n\t\tenabled: z.boolean().optional(),\n\t\tdefaultRole: roleLevel.optional(),\n\t})\n\t.meta({ id: \"AllowedDomainUpdateBody\" });\n\n// ---------------------------------------------------------------------------\n// Admin / Users: Response schemas\n// ---------------------------------------------------------------------------\n\nexport const userSchema = z\n\t.object({\n\t\tid: z.string(),\n\t\temail: z.string(),\n\t\tname: z.string().nullable(),\n\t\tavatarUrl: z.string().nullable(),\n\t\trole: z.number().int(),\n\t\temailVerified: z.boolean(),\n\t\tdisabled: z.boolean(),\n\t\tcreatedAt: z.string(),\n\t\tupdatedAt: z.string(),\n\t\tlastLogin: z.string().nullable(),\n\t\tcredentialCount: z.number().int().optional(),\n\t\toauthProviders: z.array(z.string()).optional(),\n\t})\n\t.meta({ id: \"User\" });\n\nexport const userListResponseSchema = z\n\t.object({\n\t\titems: z.array(userSchema),\n\t\tnextCursor: z.string().optional(),\n\t})\n\t.meta({ id: \"UserListResponse\" });\n\nexport const userDetailSchema = z\n\t.object({\n\t\tid: z.string(),\n\t\temail: z.string(),\n\t\tname: z.string().nullable(),\n\t\tavatarUrl: z.string().nullable(),\n\t\trole: z.number().int(),\n\t\temailVerified: z.boolean(),\n\t\tdisabled: z.boolean(),\n\t\tcreatedAt: z.string(),\n\t\tupdatedAt: z.string(),\n\t\tlastLogin: z.string().nullable(),\n\t\tcredentials: z.array(\n\t\t\tz.object({\n\t\t\t\tid: z.string(),\n\t\t\t\tname: z.string().nullable(),\n\t\t\t\tdeviceType: z.string().nullable(),\n\t\t\t\tcreatedAt: z.string(),\n\t\t\t\tlastUsedAt: z.string(),\n\t\t\t}),\n\t\t),\n\t\toauthAccounts: z.array(\n\t\t\tz.object({\n\t\t\t\tprovider: z.string(),\n\t\t\t\tcreatedAt: z.string(),\n\t\t\t}),\n\t\t),\n\t})\n\t.meta({ id: \"UserDetail\" });\n","import { z } from \"zod\";\n\n// ---------------------------------------------------------------------------\n// Widgets: Input schemas\n// ---------------------------------------------------------------------------\n\nconst widgetType = z.enum([\"content\", \"menu\", \"component\"]);\n\nexport const createWidgetAreaBody = z\n\t.object({\n\t\tname: z.string().min(1),\n\t\tlabel: z.string().min(1),\n\t\tdescription: z.string().optional(),\n\t})\n\t.meta({ id: \"CreateWidgetAreaBody\" });\n\nexport const createWidgetBody = z\n\t.object({\n\t\ttype: widgetType,\n\t\ttitle: z.string().optional(),\n\t\tcontent: z.array(z.record(z.string(), z.unknown())).optional(),\n\t\tmenuName: z.string().optional(),\n\t\tcomponentId: z.string().optional(),\n\t\tcomponentProps: z.record(z.string(), z.unknown()).optional(),\n\t})\n\t.meta({ id: \"CreateWidgetBody\" });\n\nexport const updateWidgetBody = z\n\t.object({\n\t\ttype: widgetType.optional(),\n\t\ttitle: z.string().optional(),\n\t\tcontent: z.array(z.record(z.string(), z.unknown())).optional(),\n\t\tmenuName: z.string().optional(),\n\t\tcomponentId: z.string().optional(),\n\t\tcomponentProps: z.record(z.string(), z.unknown()).optional(),\n\t})\n\t.meta({ id: \"UpdateWidgetBody\" });\n\nexport const reorderWidgetsBody = z\n\t.object({\n\t\twidgetIds: z.array(z.string().min(1)),\n\t})\n\t.meta({ id: \"ReorderWidgetsBody\" });\n\n// ---------------------------------------------------------------------------\n// Widgets: Response schemas\n// ---------------------------------------------------------------------------\n\nexport const widgetAreaSchema = z\n\t.object({\n\t\tid: z.string(),\n\t\tname: z.string(),\n\t\tlabel: z.string(),\n\t\tdescription: z.string().nullable(),\n\t\tcreated_at: z.string(),\n\t\tupdated_at: z.string(),\n\t})\n\t.meta({ id: \"WidgetArea\" });\n\nexport const widgetSchema = z\n\t.object({\n\t\tid: z.string(),\n\t\ttype: widgetType,\n\t\ttitle: z.string().optional(),\n\t\tcontent: z.array(z.record(z.string(), z.unknown())).optional(),\n\t\tmenuName: z.string().optional(),\n\t\tcomponentId: z.string().optional(),\n\t\tcomponentProps: z.record(z.string(), z.unknown()).optional(),\n\t})\n\t.meta({ id: \"Widget\" });\n\nexport const widgetAreaWithWidgetsSchema = widgetAreaSchema\n\t.extend({\n\t\twidgets: z.array(widgetSchema),\n\t})\n\t.meta({ id: \"WidgetAreaWithWidgets\" });\n\nexport const widgetAreaWithWidgetsAndCountSchema = widgetAreaWithWidgetsSchema\n\t.extend({\n\t\twidgetCount: z.number().int(),\n\t})\n\t.meta({ id: \"WidgetAreaWithWidgetsAndCount\" });\n","import { z } from \"zod\";\n\nimport { isTerminalStatus, REDIRECT_RULE_STATUSES } from \"../../redirects/status.js\";\nimport { cursorPaginationQuery } from \"./common.js\";\n\n// ---------------------------------------------------------------------------\n// Redirects: Input schemas\n// ---------------------------------------------------------------------------\n\nconst redirectType = z.coerce\n\t.number()\n\t.int()\n\t.refine((n) => REDIRECT_RULE_STATUSES.includes(n), {\n\t\tmessage: \"Redirect type must be 301, 302, 307, 308, 410, or 451\",\n\t});\n\n/** Matches CR or LF characters */\nconst CRLF = /[\\r\\n]/;\n\n/** Path must start with / and not be protocol-relative, contain no CRLF, and no path traversal */\nconst urlPath = z\n\t.string()\n\t.min(1)\n\t.refine((s) => s.startsWith(\"/\") && !s.startsWith(\"//\"), {\n\t\tmessage: \"Must be a path starting with / (no protocol-relative URLs)\",\n\t})\n\t.refine((s) => !CRLF.test(s), {\n\t\tmessage: \"URL must not contain newline characters\",\n\t})\n\t.refine(\n\t\t(s) => {\n\t\t\ttry {\n\t\t\t\treturn !decodeURIComponent(s).split(\"/\").includes(\"..\");\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t},\n\t\t{ message: \"URL must not contain path traversal segments\" },\n\t);\n\nexport const createRedirectBody = z\n\t.object({\n\t\tsource: urlPath,\n\t\t// Optional: terminal statuses (410/451) have no destination. Redirect\n\t\t// statuses require one — enforced by the refine below.\n\t\tdestination: z.union([urlPath, z.literal(\"\")]).optional(),\n\t\ttype: redirectType.optional().default(301),\n\t\tenabled: z.boolean().optional().default(true),\n\t\tgroupName: z.string().nullish(),\n\t})\n\t.refine((o) => isTerminalStatus(o.type ?? 301) || !!o.destination, {\n\t\tmessage: \"destination is required for redirect types (301, 302, 307, 308)\",\n\t\tpath: [\"destination\"],\n\t})\n\t.meta({ id: \"CreateRedirectBody\" });\n\nexport const updateRedirectBody = z\n\t.object({\n\t\tsource: urlPath.optional(),\n\t\tdestination: z.union([urlPath, z.literal(\"\")]).optional(),\n\t\ttype: redirectType.optional(),\n\t\tenabled: z.boolean().optional(),\n\t\tgroupName: z.string().nullish(),\n\t})\n\t.refine((o) => Object.values(o).some((v) => v !== undefined), {\n\t\tmessage: \"At least one field must be provided\",\n\t})\n\t.meta({ id: \"UpdateRedirectBody\" });\n\nexport const redirectsListQuery = cursorPaginationQuery\n\t.extend({\n\t\tsearch: z.string().optional(),\n\t\tgroup: z.string().optional(),\n\t\tenabled: z\n\t\t\t.enum([\"true\", \"false\"])\n\t\t\t.transform((v) => v === \"true\")\n\t\t\t.optional(),\n\t\tauto: z\n\t\t\t.enum([\"true\", \"false\"])\n\t\t\t.transform((v) => v === \"true\")\n\t\t\t.optional(),\n\t})\n\t.meta({ id: \"RedirectsListQuery\" });\n\n// ---------------------------------------------------------------------------\n// 404 Log: Input schemas\n// ---------------------------------------------------------------------------\n\nexport const notFoundListQuery = cursorPaginationQuery\n\t.extend({\n\t\tsearch: z.string().optional(),\n\t})\n\t.meta({ id: \"NotFoundListQuery\" });\n\nexport const notFoundSummaryQuery = z.object({\n\tlimit: z.coerce.number().int().min(1).max(100).optional().default(50),\n});\n\nexport const notFoundPruneBody = z\n\t.object({\n\t\tolderThan: z.string().datetime({ message: \"olderThan must be an ISO 8601 datetime\" }),\n\t})\n\t.meta({ id: \"NotFoundPruneBody\" });\n\n// ---------------------------------------------------------------------------\n// Redirects: Response schemas\n// ---------------------------------------------------------------------------\n\nexport const redirectSchema = z\n\t.object({\n\t\tid: z.string(),\n\t\tsource: z.string(),\n\t\tdestination: z.string(),\n\t\ttype: z.number().int(),\n\t\tisPattern: z.boolean(),\n\t\tenabled: z.boolean(),\n\t\thits: z.number().int(),\n\t\tlastHitAt: z.string().nullable(),\n\t\tgroupName: z.string().nullable(),\n\t\tauto: z.boolean(),\n\t\tcreatedAt: z.string(),\n\t\tupdatedAt: z.string(),\n\t})\n\t.meta({ id: \"Redirect\" });\n\nexport const redirectListResponseSchema = z\n\t.object({\n\t\titems: z.array(redirectSchema),\n\t\tnextCursor: z.string().optional(),\n\t\tloopRedirectIds: z.array(z.string()).optional(),\n\t})\n\t.meta({ id: \"RedirectListResponse\" });\n\nexport const notFoundEntrySchema = z\n\t.object({\n\t\tid: z.string(),\n\t\tpath: z.string(),\n\t\treferrer: z.string().nullable(),\n\t\tuserAgent: z.string().nullable(),\n\t\tip: z.string().nullable(),\n\t\tcreatedAt: z.string(),\n\t})\n\t.meta({ id: \"NotFoundEntry\" });\n\nexport const notFoundListResponseSchema = z\n\t.object({\n\t\titems: z.array(notFoundEntrySchema),\n\t\tnextCursor: z.string().optional(),\n\t})\n\t.meta({ id: \"NotFoundListResponse\" });\n\nexport const notFoundSummarySchema = z\n\t.object({\n\t\tpath: z.string(),\n\t\tcount: z.number().int(),\n\t\tlastSeen: z.string(),\n\t\ttopReferrer: z.string().nullable(),\n\t})\n\t.meta({ id: \"NotFoundSummary\" });\n\nexport const notFoundSummaryResponseSchema = z\n\t.object({ items: z.array(notFoundSummarySchema) })\n\t.meta({ id: \"NotFoundSummaryResponse\" });\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"relations-BKvO39-9.mjs","names":[],"sources":["../src/api/handlers/relations.ts"],"sourcesContent":["import type { Kysely } from \"kysely\";\n\nimport { ContentRepository } from \"../../database/repositories/content.js\";\nimport {\n\tRelationRepository,\n\ttype ContentReference,\n\ttype CreateRelationInput,\n\ttype Relation,\n} from \"../../database/repositories/relation.js\";\nimport { InvalidCursorError } from \"../../database/repositories/types.js\";\nimport type { ContentItem } from \"../../database/repositories/types.js\";\nimport type { Database } from \"../../database/types.js\";\nimport { SchemaRegistry } from \"../../schema/registry.js\";\nimport type { ApiResult } from \"../types.js\";\n\n/** Map an edge-read failure: a bad pagination cursor is a 400 client error,\n * everything else is the generic 500-shaped reference-read error. */\nfunction referencesGetError(error: unknown): ApiResult<never> {\n\tif (error instanceof InvalidCursorError) {\n\t\treturn { success: false, error: { code: \"INVALID_CURSOR\", message: error.message } };\n\t}\n\treturn {\n\t\tsuccess: false,\n\t\terror: { code: \"REFERENCES_GET_ERROR\", message: \"Failed to get references\" },\n\t};\n}\n\n/** True for SQLite UNIQUE / Postgres unique_violation messages (matches the\n * fingerprint used in the content handlers). Narrow enough not to catch NOT\n * NULL / CHECK violations whose messages also say \"constraint\". */\nfunction isUniqueViolation(error: unknown): boolean {\n\tconst message = error instanceof Error ? error.message.toLowerCase() : \"\";\n\treturn message.includes(\"unique constraint failed\") || message.includes(\"duplicate key\");\n}\n\nexport async function handleRelationCreate(\n\tdb: Kysely<Database>,\n\tinput: CreateRelationInput,\n): Promise<ApiResult<{ relation: Relation }>> {\n\ttry {\n\t\tconst repo = new RelationRepository(db);\n\n\t\t// Invariant: a relation must point at collections that exist. There is no\n\t\t// SQL FK (group-linking precludes it), so a ghost collection would yield a\n\t\t// structurally-valid-but-permanently-useless relation. Skip when\n\t\t// `translationOf` is set — structural fields are then inherited from an\n\t\t// already-validated source, and the input collections are ignored.\n\t\tif (!input.translationOf) {\n\t\t\tif (!input.parentCollection || !input.childCollection) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: {\n\t\t\t\t\t\tcode: \"VALIDATION_ERROR\",\n\t\t\t\t\t\tmessage:\n\t\t\t\t\t\t\t\"parentCollection and childCollection are required unless translationOf is set\",\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t\tconst registry = new SchemaRegistry(db);\n\t\t\tfor (const collection of [input.parentCollection, input.childCollection]) {\n\t\t\t\tif (!(await registry.getCollection(collection))) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tsuccess: false,\n\t\t\t\t\t\terror: {\n\t\t\t\t\t\t\tcode: \"COLLECTION_NOT_FOUND\",\n\t\t\t\t\t\t\tmessage: `Collection '${collection}' not found`,\n\t\t\t\t\t\t},\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst relation = await repo.create(input);\n\t\treturn { success: true, data: { relation } };\n\t} catch (error) {\n\t\t// A bad `translationOf` makes the repo throw loudly rather than mint an\n\t\t// unlinked relation — surface it as 404, not a generic 500.\n\t\tif (\n\t\t\terror instanceof Error &&\n\t\t\terror.message.includes(\"Source relation for translation not found\")\n\t\t) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: \"Source relation for translation not found\" },\n\t\t\t};\n\t\t}\n\t\t// UNIQUE(name, locale) collision, or a second translation for an\n\t\t// already-present (translation_group, locale) — both are client conflicts.\n\t\tif (isUniqueViolation(error)) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"CONFLICT\",\n\t\t\t\t\tmessage: \"A relation with this name or locale already exists\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"RELATION_CREATE_ERROR\", message: \"Failed to create relation\" },\n\t\t};\n\t}\n}\n\nexport async function handleRelationGet(\n\tdb: Kysely<Database>,\n\tid: string,\n): Promise<ApiResult<{ relation: Relation }>> {\n\ttry {\n\t\tconst repo = new RelationRepository(db);\n\t\tconst relation = await repo.findById(id);\n\t\tif (!relation) {\n\t\t\treturn { success: false, error: { code: \"NOT_FOUND\", message: \"Relation not found\" } };\n\t\t}\n\t\treturn { success: true, data: { relation } };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"RELATION_GET_ERROR\", message: \"Failed to get relation\" },\n\t\t};\n\t}\n}\n\nexport async function handleRelationList(\n\tdb: Kysely<Database>,\n\topts: { locale?: string },\n): Promise<ApiResult<{ relations: Relation[] }>> {\n\ttry {\n\t\tconst repo = new RelationRepository(db);\n\t\tconst relations = await repo.list(opts.locale);\n\t\treturn { success: true, data: { relations } };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"RELATION_LIST_ERROR\", message: \"Failed to list relations\" },\n\t\t};\n\t}\n}\n\nexport async function handleRelationUpdate(\n\tdb: Kysely<Database>,\n\tid: string,\n\tinput: { parentLabel?: string; childLabel?: string },\n): Promise<ApiResult<{ relation: Relation }>> {\n\ttry {\n\t\tconst repo = new RelationRepository(db);\n\t\tconst relation = await repo.update(id, input);\n\t\tif (!relation) {\n\t\t\treturn { success: false, error: { code: \"NOT_FOUND\", message: \"Relation not found\" } };\n\t\t}\n\t\treturn { success: true, data: { relation } };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"RELATION_UPDATE_ERROR\", message: \"Failed to update relation\" },\n\t\t};\n\t}\n}\n\nexport async function handleRelationDelete(\n\tdb: Kysely<Database>,\n\tid: string,\n): Promise<ApiResult<{ deleted: true }>> {\n\ttry {\n\t\tconst repo = new RelationRepository(db);\n\t\tconst deleted = await repo.delete(id);\n\t\tif (!deleted) {\n\t\t\treturn { success: false, error: { code: \"NOT_FOUND\", message: \"Relation not found\" } };\n\t\t}\n\t\treturn { success: true, data: { deleted: true } };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"RELATION_DELETE_ERROR\", message: \"Failed to delete relation\" },\n\t\t};\n\t}\n}\n\nexport async function handleRelationTranslations(\n\tdb: Kysely<Database>,\n\tid: string,\n): Promise<\n\tApiResult<{\n\t\ttranslationGroup: string;\n\t\ttranslations: {\n\t\t\tid: string;\n\t\t\tname: string;\n\t\t\tlocale: string;\n\t\t\tparentLabel: string;\n\t\t\tchildLabel: string;\n\t\t}[];\n\t}>\n> {\n\ttry {\n\t\tconst repo = new RelationRepository(db);\n\t\tconst relation = await repo.findById(id);\n\t\tif (!relation) {\n\t\t\treturn { success: false, error: { code: \"NOT_FOUND\", message: \"Relation not found\" } };\n\t\t}\n\t\tconst siblings = await repo.findTranslations(relation.translationGroup);\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\ttranslationGroup: relation.translationGroup,\n\t\t\t\ttranslations: siblings.map((r) => ({\n\t\t\t\t\tid: r.id,\n\t\t\t\t\tname: r.name,\n\t\t\t\t\tlocale: r.locale,\n\t\t\t\t\tparentLabel: r.parentLabel,\n\t\t\t\t\tchildLabel: r.childLabel,\n\t\t\t\t})),\n\t\t\t},\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"RELATION_TRANSLATIONS_ERROR\", message: \"Failed to get translations\" },\n\t\t};\n\t}\n}\n\nexport type EntryRef = {\n\tid: string;\n\tslug: string | null;\n\tcollection: string;\n\t/** The actual locale of the resolved variant — see `pickVariant`. */\n\tlocale: string | null;\n\tsortOrder?: number;\n};\n\n/** Resolve a relation from an id OR its translation_group. */\nasync function resolveRelation(\n\trepo: RelationRepository,\n\tidOrGroup: string,\n): Promise<Relation | null> {\n\tconst byId = await repo.findById(idOrGroup);\n\tif (byId) return byId;\n\tconst group = await repo.findTranslations(idOrGroup);\n\treturn group[0] ?? null;\n}\n\n/**\n * Pick the locale variant matching `locale`, falling back to the first entry\n * (lowest locale code). The fallback is intentional — an edge is keyed by\n * `translation_group`, so a referenced entry that exists only in another locale\n * is still a real reference — but the returned ref carries the variant's actual\n * `locale` so callers never mistake a fallback for the requested locale.\n */\nfunction pickVariant(items: ContentItem[], locale: string | null): ContentItem | undefined {\n\treturn items.find((i) => i.locale === locale) ?? items[0];\n}\n\n/**\n * Resolve edge groups to loadable entries in `collection` at `locale`.\n * Dangling groups (no surviving entry) are skipped — cleanup is a later slice.\n *\n * All groups are loaded in one batched query (chunked at `SQL_BATCH_SIZE`)\n * rather than a `findTranslations` per edge, so a parent with N children costs\n * a constant number of queries, not N+1. Edge order (the caller's `sort_order`)\n * is preserved by iterating `edges`.\n *\n * `includeDrafts` is false for callers without `content:read_drafts`: the load\n * is restricted to published entries so a draft/scheduled entry referenced by an\n * edge is skipped exactly like a dangling one, never leaking its id/slug/locale.\n */\nasync function resolveEntries(\n\tcontent: ContentRepository,\n\tcollection: string,\n\tedges: ContentReference[],\n\tpick: (e: ContentReference) => string,\n\tlocale: string | null,\n\tincludeDrafts: boolean,\n): Promise<EntryRef[]> {\n\tconst groups = edges.map(pick);\n\tconst all = await content.findTranslationsForGroups(collection, groups, {\n\t\tpublishedOnly: !includeDrafts,\n\t});\n\n\t// Group the flat variant list by translation_group so each edge can pick its\n\t// own locale variant.\n\tconst variantsByGroup = new Map<string, ContentItem[]>();\n\tfor (const item of all) {\n\t\tif (item.translationGroup == null) continue;\n\t\tconst list = variantsByGroup.get(item.translationGroup);\n\t\tif (list) list.push(item);\n\t\telse variantsByGroup.set(item.translationGroup, [item]);\n\t}\n\n\tconst refs: EntryRef[] = [];\n\tfor (const edge of edges) {\n\t\tconst variants = variantsByGroup.get(pick(edge));\n\t\tif (!variants) continue;\n\t\tconst entry = pickVariant(variants, locale);\n\t\tif (!entry) continue;\n\t\trefs.push({\n\t\t\tid: entry.id,\n\t\t\tslug: entry.slug,\n\t\t\tcollection,\n\t\t\tlocale: entry.locale,\n\t\t\tsortOrder: edge.sortOrder,\n\t\t});\n\t}\n\treturn refs;\n}\n\n/** Pagination inputs for the edge read endpoints. */\nexport type PageOptions = { limit?: number; cursor?: string };\n\nexport async function handleReferenceChildrenGet(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tentryId: string,\n\trelation: string,\n\tpage: PageOptions = {},\n\tincludeDrafts = false,\n): Promise<ApiResult<{ children: EntryRef[]; nextCursor?: string }>> {\n\ttry {\n\t\tconst repo = new RelationRepository(db);\n\t\tconst content = new ContentRepository(db);\n\n\t\tconst rel = await resolveRelation(repo, relation);\n\t\tif (!rel)\n\t\t\treturn { success: false, error: { code: \"NOT_FOUND\", message: \"Relation not found\" } };\n\t\tif (collection !== rel.parentCollection) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"VALIDATION_ERROR\",\n\t\t\t\t\tmessage: \"Entry is not the parent side of this relation\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst entry = await content.findByIdOrSlug(collection, entryId);\n\t\t// A caller without draft access must not anchor on a non-published entry —\n\t\t// return NOT_FOUND (not 403) so they can't probe draft ids by status code,\n\t\t// mirroring the single-item content read.\n\t\tif (!entry?.translationGroup || (!includeDrafts && entry.status !== \"published\")) {\n\t\t\treturn { success: false, error: { code: \"NOT_FOUND\", message: \"Content entry not found\" } };\n\t\t}\n\n\t\tconst edges = await repo.getChildrenPage(rel.translationGroup, entry.translationGroup, page);\n\t\tconst children = await resolveEntries(\n\t\t\tcontent,\n\t\t\trel.childCollection,\n\t\t\tedges.items,\n\t\t\t(e) => e.childGroup,\n\t\t\tentry.locale,\n\t\t\tincludeDrafts,\n\t\t);\n\t\treturn { success: true, data: { children, nextCursor: edges.nextCursor } };\n\t} catch (error) {\n\t\treturn referencesGetError(error);\n\t}\n}\n\nexport async function handleReferenceChildrenSet(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tentryId: string,\n\trelation: string,\n\tchildIds: string[],\n): Promise<ApiResult<{ children: EntryRef[]; nextCursor?: string }>> {\n\ttry {\n\t\tconst repo = new RelationRepository(db);\n\t\tconst content = new ContentRepository(db);\n\n\t\tconst rel = await resolveRelation(repo, relation);\n\t\tif (!rel)\n\t\t\treturn { success: false, error: { code: \"NOT_FOUND\", message: \"Relation not found\" } };\n\t\tif (collection !== rel.parentCollection) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"VALIDATION_ERROR\",\n\t\t\t\t\tmessage: \"Entry is not the parent side of this relation\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst entry = await content.findByIdOrSlug(collection, entryId);\n\t\tif (!entry?.translationGroup) {\n\t\t\treturn { success: false, error: { code: \"NOT_FOUND\", message: \"Content entry not found\" } };\n\t\t}\n\n\t\t// Resolve every child within the relation's child_collection in one batch\n\t\t// (constant queries, not an N+1 of point lookups for a set up to 1000). A\n\t\t// child id that does not resolve there fails collection-agreement\n\t\t// (invariant 3); order is preserved by iterating the caller's `childIds`.\n\t\tconst resolvedChildren = await content.findManyByIdOrSlug(rel.childCollection, childIds);\n\t\tconst childGroups: string[] = [];\n\t\tfor (const childId of childIds) {\n\t\t\tconst child = resolvedChildren.get(childId);\n\t\t\tif (!child?.translationGroup) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: {\n\t\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\t\tmessage: `Child entry '${childId}' not found in ${rel.childCollection}`,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t\tchildGroups.push(child.translationGroup);\n\t\t}\n\n\t\tawait repo.setChildren(rel.translationGroup, entry.translationGroup, childGroups);\n\n\t\t// Return the first page of the new set, mirroring the GET shape. The actor\n\t\t// holds an edit permission (gated by the route), so draft children are\n\t\t// included in the echo.\n\t\tconst edges = await repo.getChildrenPage(rel.translationGroup, entry.translationGroup);\n\t\tconst children = await resolveEntries(\n\t\t\tcontent,\n\t\t\trel.childCollection,\n\t\t\tedges.items,\n\t\t\t(e) => e.childGroup,\n\t\t\tentry.locale,\n\t\t\ttrue,\n\t\t);\n\t\treturn { success: true, data: { children, nextCursor: edges.nextCursor } };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"REFERENCES_SET_ERROR\", message: \"Failed to set references\" },\n\t\t};\n\t}\n}\n\nexport async function handleReferenceParentsGet(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tentryId: string,\n\trelation: string,\n\tpage: PageOptions = {},\n\tincludeDrafts = false,\n): Promise<ApiResult<{ parents: EntryRef[]; nextCursor?: string }>> {\n\ttry {\n\t\tconst repo = new RelationRepository(db);\n\t\tconst content = new ContentRepository(db);\n\n\t\tconst rel = await resolveRelation(repo, relation);\n\t\tif (!rel)\n\t\t\treturn { success: false, error: { code: \"NOT_FOUND\", message: \"Relation not found\" } };\n\t\tif (collection !== rel.childCollection) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"VALIDATION_ERROR\",\n\t\t\t\t\tmessage: \"Entry is not the child side of this relation\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst entry = await content.findByIdOrSlug(collection, entryId);\n\t\t// Same draft-anchor guard as the children read: a non-draft-reader anchoring\n\t\t// on an unpublished entry gets NOT_FOUND, not its backlinks.\n\t\tif (!entry?.translationGroup || (!includeDrafts && entry.status !== \"published\")) {\n\t\t\treturn { success: false, error: { code: \"NOT_FOUND\", message: \"Content entry not found\" } };\n\t\t}\n\n\t\tconst edges = await repo.getParentsPage(rel.translationGroup, entry.translationGroup, page);\n\t\tconst parents = await resolveEntries(\n\t\t\tcontent,\n\t\t\trel.parentCollection,\n\t\t\tedges.items,\n\t\t\t(e) => e.parentGroup,\n\t\t\tentry.locale,\n\t\t\tincludeDrafts,\n\t\t);\n\t\treturn { success: true, data: { parents, nextCursor: edges.nextCursor } };\n\t} catch (error) {\n\t\treturn referencesGetError(error);\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"relations-Bh8jlFsU.d.mts","names":[],"sources":["../src/api/schemas/common.ts","../src/api/schemas/content.ts","../src/api/schemas/media.ts","../src/api/schemas/media-usage.ts","../src/api/schemas/schema.ts","../src/api/schemas/comments.ts","../src/api/schemas/auth.ts","../src/api/schemas/menus.ts","../src/api/schemas/taxonomies.ts","../src/api/schemas/sections.ts","../src/api/schemas/settings.ts","../src/api/schemas/search.ts","../src/api/schemas/import.ts","../src/api/schemas/setup.ts","../src/api/schemas/users.ts","../src/api/schemas/widgets.ts","../src/api/schemas/redirects.ts","../src/api/schemas/bylines.ts","../src/api/schemas/byline-fields.ts","../src/api/schemas/relations.ts"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"search-Bff-7jFt.mjs","names":[],"sources":["../src/search/query.ts","../src/search/text-extraction.ts","../src/search/index.ts"],"sourcesContent":["/**\n * Search Query Functions\n *\n * Programmatic API for searching content using FTS5.\n */\n\nimport type { Kysely } from \"kysely\";\nimport { sql } from \"kysely\";\n\nimport { encodeCursor, decodeCursor, InvalidCursorError } from \"../database/repositories/types.js\";\nimport type { Database } from \"../database/types.js\";\nimport { validateIdentifier } from \"../database/validate.js\";\nimport { getDb } from \"../loader.js\";\nimport { FTSManager } from \"./fts-manager.js\";\nimport type {\n\tSearchOptions,\n\tCollectionSearchOptions,\n\tSearchResult,\n\tSearchResponse,\n\tSuggestOptions,\n\tSuggestion,\n\tSearchStats,\n} from \"./types.js\";\n\n/**\n * Marker stored in the `id` slot of a search pagination cursor.\n *\n * Search results are merged from per-collection FTS queries and re-sorted by\n * score, so there is no single stable keyset column to encode (the way\n * `getEmDashCollection` encodes `(orderValue, id)`). We page over the merged,\n * score-sorted set by offset instead, carried as the cursor's `orderValue`.\n * Reusing `encodeCursor`/`decodeCursor` keeps the opaque base64-JSON shape and\n * the `InvalidCursorError` handling consistent with the rest of the API.\n */\nconst SEARCH_CURSOR_MARKER = \"search\";\n\n/** Encode the next-page offset into an opaque search cursor. */\nfunction encodeSearchCursor(offset: number): string {\n\treturn encodeCursor(String(offset), SEARCH_CURSOR_MARKER);\n}\n\n/**\n * Decode a search cursor back to its offset. Throws `InvalidCursorError` for a\n * malformed cursor or one that doesn't carry a non-negative integer offset, so\n * a client pagination bug surfaces immediately rather than silently restarting\n * from the first page.\n */\n/** Upper bound on a decoded search offset, to cap the per-collection row fetch a forged cursor can trigger. */\nconst MAX_SEARCH_OFFSET = 10_000;\n\nfunction decodeSearchOffset(cursor: string): number {\n\tconst { orderValue, id } = decodeCursor(cursor);\n\tif (id !== SEARCH_CURSOR_MARKER) {\n\t\tthrow new InvalidCursorError(cursor);\n\t}\n\tconst offset = Number(orderValue);\n\tif (!Number.isInteger(offset) || offset < 0 || offset > MAX_SEARCH_OFFSET) {\n\t\tthrow new InvalidCursorError(cursor);\n\t}\n\treturn offset;\n}\n\n/** Pattern to split on whitespace for query term extraction */\nconst WHITESPACE_SPLIT_PATTERN = /\\s+/;\nconst FTS_OPERATORS_PATTERN = /\\b(AND|OR|NOT|NEAR)\\b/i;\nconst DOUBLE_QUOTE_PATTERN = /\"/g;\n\n/**\n * Detect FTS5 query syntax errors. Match specifically on the SQLite FTS5\n * error fingerprints rather than a broad \"fts5\" / \"syntax error\" filter\n * (which would also swallow internal table-corruption errors). The two\n * fingerprints we care about are:\n *\n * - \"fts5: syntax error near …\" — unbalanced quotes, stray operators,\n * other malformed user input\n * - \"unknown special query: …\" — bare special tokens like `^*` that\n * parse but don't resolve to a real FTS5 directive\n */\nfunction isFts5SyntaxError(error: unknown): boolean {\n\tif (!(error instanceof Error)) return false;\n\tconst message = error.message.toLowerCase();\n\treturn message.includes(\"fts5: syntax error\") || message.includes(\"unknown special query\");\n}\n\n/**\n * Search across multiple collections\n *\n * Public API that auto-injects the database.\n *\n * @param query - Search query (FTS5 syntax supported)\n * @param options - Search options\n * @returns Search results with pagination\n *\n * @example\n * ```typescript\n * import { search } from \"emdash\";\n *\n * const results = await search(\"hello world\", {\n * collections: [\"posts\", \"pages\"],\n * limit: 20\n * });\n * ```\n */\nexport async function search(query: string, options: SearchOptions = {}): Promise<SearchResponse> {\n\tconst db = await getDb();\n\treturn searchWithDb(db, query, options);\n}\n\n/**\n * Search across multiple collections (with explicit db)\n *\n * @internal Use `search()` in templates. This variant is for admin routes\n * that already have a database handle.\n *\n * @param db - Kysely database instance\n * @param query - Search query (FTS5 syntax supported)\n * @param options - Search options\n * @returns Search results with pagination\n */\nexport async function searchWithDb(\n\tdb: Kysely<Database>,\n\tquery: string,\n\toptions: SearchOptions = {},\n): Promise<SearchResponse> {\n\tconst ftsManager = new FTSManager(db);\n\tconst limit = options.limit ?? 20;\n\tconst status = options.status ?? \"published\";\n\tconst offset = options.cursor ? decodeSearchOffset(options.cursor) : 0;\n\n\t// Get searchable collections\n\tlet collections = options.collections;\n\tif (!collections || collections.length === 0) {\n\t\tcollections = await getSearchableCollections(db);\n\t}\n\n\tif (collections.length === 0) {\n\t\treturn { items: [] };\n\t}\n\n\t// To rank the merged window [offset, offset + limit) correctly, each\n\t// collection must contribute its own top (offset + limit) rows — in the\n\t// worst case the entire window comes from one collection. The extra +1\n\t// row detects whether a further page exists.\n\tconst perCollectionLimit = offset + limit + 1;\n\n\t// Search each collection and merge results\n\tconst allResults: SearchResult[] = [];\n\tconst titleColumns = await ftsManager.getCollectionsWithTitleColumn(collections);\n\n\tfor (const collection of collections) {\n\t\tconst config = await ftsManager.getSearchConfig(collection);\n\t\tif (!config?.enabled) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst collectionResults = await searchSingleCollection(\n\t\t\tdb,\n\t\t\tcollection,\n\t\t\tquery,\n\t\t\t{\n\t\t\t\tstatus,\n\t\t\t\tlocale: options.locale,\n\t\t\t\tlimit: perCollectionLimit,\n\t\t\t},\n\t\t\tconfig.weights,\n\t\t\ttitleColumns.has(collection),\n\t\t);\n\n\t\tallResults.push(...collectionResults);\n\t}\n\n\t// Sort by score descending\n\tallResults.sort((a, b) => b.score - a.score);\n\n\t// Page the merged set by offset. A nextCursor is issued only when at least\n\t// one result exists past this page.\n\tconst items = allResults.slice(offset, offset + limit);\n\tconst hasMore = allResults.length > offset + limit;\n\tconst nextCursor = hasMore ? encodeSearchCursor(offset + limit) : undefined;\n\n\treturn { items, nextCursor };\n}\n\n/**\n * Search within a single collection\n *\n * @param db - Kysely database instance\n * @param collection - Collection slug\n * @param query - Search query (FTS5 syntax supported)\n * @param options - Search options\n * @returns Search results with pagination\n *\n * @example\n * ```typescript\n * const results = await searchCollection(db, \"posts\", \"hello world\", {\n * limit: 10\n * });\n * ```\n */\nexport async function searchCollection(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tquery: string,\n\toptions: CollectionSearchOptions = {},\n): Promise<SearchResponse> {\n\tconst ftsManager = new FTSManager(db);\n\tconst config = await ftsManager.getSearchConfig(collection);\n\n\tif (!config?.enabled) {\n\t\treturn { items: [] };\n\t}\n\n\tconst limit = options.limit ?? 20;\n\tconst offset = options.cursor ? decodeSearchOffset(options.cursor) : 0;\n\n\t// Over-fetch the [offset, offset + limit) window plus one row to detect a\n\t// further page, then slice. Keeps the FTS SQL (LIMIT-only) unchanged and\n\t// the cursor contract identical to the cross-collection path.\n\tconst fetched = await searchSingleCollection(\n\t\tdb,\n\t\tcollection,\n\t\tquery,\n\t\t{ status: options.status, locale: options.locale, limit: offset + limit + 1 },\n\t\tconfig.weights,\n\t);\n\n\tconst items = fetched.slice(offset, offset + limit);\n\tconst hasMore = fetched.length > offset + limit;\n\tconst nextCursor = hasMore ? encodeSearchCursor(offset + limit) : undefined;\n\n\treturn { items, nextCursor };\n}\n\n/**\n * Internal function to search a single collection\n */\nasync function searchSingleCollection(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tquery: string,\n\toptions: CollectionSearchOptions,\n\tweights?: Record<string, number>,\n\thasTitle?: boolean,\n): Promise<SearchResult[]> {\n\t// Validate before any raw SQL interpolation\n\tvalidateIdentifier(collection, \"collection slug\");\n\n\tconst ftsManager = new FTSManager(db);\n\tconst ftsTable = ftsManager.getFtsTableName(collection);\n\tconst contentTable = ftsManager.getContentTableName(collection);\n\tconst limit = options.limit ?? 20;\n\tconst status = options.status ?? \"published\";\n\tconst locale = options.locale;\n\n\t// Check if FTS table exists\n\tif (!(await ftsManager.ftsTableExists(collection))) {\n\t\treturn [];\n\t}\n\n\t// Escape the query for FTS5\n\tconst escapedQuery = escapeQuery(query);\n\tif (!escapedQuery) {\n\t\treturn [];\n\t}\n\n\t// Get searchable fields for snippet generation\n\tconst searchableFields = await ftsManager.getSearchableFields(collection);\n\n\t// `title` is an optional user-defined field, not a system column. Only\n\t// select it when the collection actually has one; otherwise the query\n\t// errors with \"no such column: c.title\" (#1178). Multi-collection callers\n\t// precompute this in bulk and pass it in; single-collection callers fall\n\t// back to the per-collection check.\n\tconst collectionHasTitle = hasTitle ?? (await ftsManager.hasTitleColumn(collection));\n\tconst titleExpr = collectionHasTitle ? sql`c.title` : sql`NULL`;\n\n\t// Build weight string for bm25 if weights provided\n\t// Format: bm25(table, weight1, weight2, ...)\n\t// First two weights are for 'id' and 'locale' columns (UNINDEXED, so 0)\n\tlet bm25Args = \"\";\n\tif (weights && searchableFields.length > 0) {\n\t\tconst weightValues = [\"0\", \"0\"]; // id column, locale column\n\t\tfor (const field of searchableFields) {\n\t\t\tweightValues.push(String(weights[field] ?? 1));\n\t\t}\n\t\tbm25Args = weightValues.join(\", \");\n\t}\n\n\t// Build and execute the search query\n\t// Using raw SQL because Kysely doesn't have FTS5 support\n\tconst bm25Expr = bm25Args ? `bm25(\"${ftsTable}\", ${bm25Args})` : `bm25(\"${ftsTable}\")`;\n\n\t// Snippet column index is 2 (after id=0, locale=1, first searchable field=2)\n\tlet results;\n\ttry {\n\t\tresults = await sql<{\n\t\t\tid: string;\n\t\t\tslug: string | null;\n\t\t\tlocale: string;\n\t\t\ttitle: string | null;\n\t\t\tsnippet: string | null;\n\t\t\tscore: number;\n\t\t}>`\n\t\tSELECT\n\t\t\tc.id,\n\t\t\tc.slug,\n\t\t\tc.locale,\n\t\t\t${titleExpr} as title,\n\t\t\tsnippet(\"${sql.raw(ftsTable)}\", 2, '<mark>', '</mark>', '...', 32) as snippet,\n\t\t\t${sql.raw(bm25Expr)} as score\n\t\tFROM \"${sql.raw(ftsTable)}\" f\n\t\tJOIN \"${sql.raw(contentTable)}\" c ON f.id = c.id\n\t\tWHERE \"${sql.raw(ftsTable)}\" MATCH ${escapedQuery}\n\t\tAND c.status = ${status}\n\t\tAND c.deleted_at IS NULL\n\t\t${locale ? sql`AND c.locale = ${locale}` : sql``}\n\t\tORDER BY score\n\t\tLIMIT ${limit}\n\t`.execute(db);\n\t} catch (error) {\n\t\t// FTS5 returns syntax errors for queries with unbalanced quotes,\n\t\t// stray operators, or other malformed input. Treat these as\n\t\t// \"no matches\" so the user gets an empty result rather than an\n\t\t// internals-leaking error. Other errors (table missing, IO) still\n\t\t// propagate. Intentionally not logged: any anonymous client can\n\t\t// trigger this path, and the underlying error message embeds the\n\t\t// raw query, so logging would be both noisy and a log-injection\n\t\t// vector.\n\t\tif (isFts5SyntaxError(error)) {\n\t\t\treturn [];\n\t\t}\n\t\tthrow error;\n\t}\n\n\treturn results.rows.map((row) => ({\n\t\tcollection,\n\t\tid: row.id,\n\t\tslug: row.slug,\n\t\tlocale: row.locale,\n\t\ttitle: row.title ?? undefined,\n\t\t// SQLite's snippet() returns NULL when the targeted column is\n\t\t// NULL for that row — even if the row matched via a different\n\t\t// searchable column. Skip sanitization in that case so we don't\n\t\t// throw on `null.replace`. The SearchResult.snippet field is\n\t\t// already optional, so omitting it is the documented contract.\n\t\tsnippet: row.snippet === null ? undefined : sanitizeSnippet(row.snippet),\n\t\tscore: Math.abs(row.score), // bm25 returns negative scores\n\t}));\n}\n\n// Module-scope regexes so the engine doesn't recompile per call —\n// snippet sanitization runs on every search result.\nconst SNIPPET_AMP_RE = /&/g;\nconst SNIPPET_LT_RE = /</g;\nconst SNIPPET_GT_RE = />/g;\nconst SNIPPET_QUOT_RE = /\"/g;\nconst SNIPPET_APOS_RE = /'/g;\n\n/**\n * Make an FTS5 snippet safe to render with `set:html` / `innerHTML`.\n *\n * SQLite's `snippet()` function splices literal `<mark>` and `</mark>`\n * markers around matched terms but does not escape the surrounding\n * source text. Posts that legitimately contain `<`, `>`, `&`, `\"` or\n * `'` would render as broken markup, and a `<script>` literal in a\n * title (or any other indexed field) would execute when displayed.\n *\n * The fix: HTML-escape the whole string, which turns the markers into\n * `<mark>` / `</mark>`. Then restore those two patterns to\n * their original tag form. The result is \"the indexed text with all\n * HTML metacharacters escaped, plus a small set of literal `<mark>`\n * highlight tags around matched terms\" — which matches the API's\n * documented contract.\n */\nfunction sanitizeSnippet(snippet: string): string {\n\treturn snippet\n\t\t.replace(SNIPPET_AMP_RE, \"&\")\n\t\t.replace(SNIPPET_LT_RE, \"<\")\n\t\t.replace(SNIPPET_GT_RE, \">\")\n\t\t.replace(SNIPPET_QUOT_RE, \""\")\n\t\t.replace(SNIPPET_APOS_RE, \"'\")\n\t\t.replaceAll(\"<mark>\", \"<mark>\")\n\t\t.replaceAll(\"</mark>\", \"</mark>\");\n}\n\n/**\n * Get search suggestions for autocomplete\n *\n * @param db - Kysely database instance\n * @param query - Partial search query\n * @param options - Suggestion options\n * @returns Array of suggestions\n */\nexport async function getSuggestions(\n\tdb: Kysely<Database>,\n\tquery: string,\n\toptions: SuggestOptions = {},\n): Promise<Suggestion[]> {\n\tconst limit = options.limit ?? 5;\n\tconst locale = options.locale;\n\n\t// Get searchable collections\n\tlet collections = options.collections;\n\tif (!collections || collections.length === 0) {\n\t\tcollections = await getSearchableCollections(db);\n\t}\n\n\tif (collections.length === 0) {\n\t\treturn [];\n\t}\n\n\tconst suggestions: Suggestion[] = [];\n\tconst ftsManager = new FTSManager(db);\n\tconst titleColumns = await ftsManager.getCollectionsWithTitleColumn(collections);\n\n\tfor (const collection of collections) {\n\t\tconst config = await ftsManager.getSearchConfig(collection);\n\t\tif (!config?.enabled) {\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Suggestions are title-based (Suggestion.title is required and the\n\t\t// query filters on `c.title IS NOT NULL`). Collections without a\n\t\t// `title` field can't produce one, and selecting `c.title` would\n\t\t// error, so skip them. See #1178.\n\t\tif (!titleColumns.has(collection)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Validate before raw SQL interpolation\n\t\tvalidateIdentifier(collection, \"collection slug\");\n\n\t\tconst ftsTable = ftsManager.getFtsTableName(collection);\n\t\tconst contentTable = ftsManager.getContentTableName(collection);\n\n\t\t// Use prefix search for autocomplete. `escapeQuery` already appends `*`\n\t\t// to each term for prefix matching, so we must not append another one.\n\t\tconst prefixQuery = escapeQuery(query);\n\t\tif (!prefixQuery) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tlet results;\n\t\ttry {\n\t\t\tresults = await sql<{\n\t\t\t\tid: string;\n\t\t\t\tslug: string | null;\n\t\t\t\ttitle: string;\n\t\t\t}>`\n\t\t\t\tSELECT \n\t\t\t\t\tc.id,\n\t\t\t\t\tc.slug,\n\t\t\t\t\tc.title\n\t\t\t\tFROM \"${sql.raw(ftsTable)}\" f\n\t\t\t\tJOIN \"${sql.raw(contentTable)}\" c ON f.id = c.id\n\t\t\t\tWHERE \"${sql.raw(ftsTable)}\" MATCH ${prefixQuery}\n\t\t\t\tAND c.status = 'published'\n\t\t\t\tAND c.deleted_at IS NULL\n\t\t\t\tAND c.title IS NOT NULL\n\t\t\t\t${locale ? sql`AND c.locale = ${locale}` : sql``}\n\t\t\t\tORDER BY bm25(\"${sql.raw(ftsTable)}\")\n\t\t\t\tLIMIT ${limit}\n\t\t\t`.execute(db);\n\t\t} catch (error) {\n\t\t\t// Same swallow as searchSingleCollection: malformed prefix\n\t\t\t// queries should yield no suggestions, not surface DB errors.\n\t\t\t// Intentionally not logged (anonymous-triggerable, echoes\n\t\t\t// user input -- see searchSingleCollection for rationale).\n\t\t\tif (isFts5SyntaxError(error)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\n\t\tfor (const row of results.rows) {\n\t\t\tsuggestions.push({\n\t\t\t\tcollection,\n\t\t\t\tid: row.id,\n\t\t\t\tslug: row.slug,\n\t\t\t\ttitle: row.title,\n\t\t\t});\n\t\t}\n\t}\n\n\treturn suggestions.slice(0, limit);\n}\n\n/**\n * Get search statistics for all collections\n */\nexport async function getSearchStats(db: Kysely<Database>): Promise<SearchStats> {\n\tconst ftsManager = new FTSManager(db);\n\tconst collections = await getSearchableCollections(db);\n\tconst stats: SearchStats = { collections: {} };\n\n\tfor (const collection of collections) {\n\t\tconst collectionStats = await ftsManager.getIndexStats(collection);\n\t\tif (collectionStats) {\n\t\t\tstats.collections[collection] = collectionStats;\n\t\t}\n\t}\n\n\treturn stats;\n}\n\n/**\n * Get list of collections with search enabled\n */\nasync function getSearchableCollections(db: Kysely<Database>): Promise<string[]> {\n\tconst results = await db\n\t\t.selectFrom(\"_emdash_collections\")\n\t\t.select([\"slug\", \"search_config\"])\n\t\t.execute();\n\n\treturn results\n\t\t.filter((r) => {\n\t\t\tif (!r.search_config) return false;\n\t\t\ttry {\n\t\t\t\tconst config = JSON.parse(r.search_config);\n\t\t\t\treturn config.enabled === true;\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t})\n\t\t.map((r) => r.slug);\n}\n\n/**\n * Escape a query string for FTS5\n *\n * Handles special characters and prevents injection.\n */\nfunction escapeQuery(query: string): string {\n\tif (!query || typeof query !== \"string\") {\n\t\treturn \"\";\n\t}\n\n\t// Trim whitespace\n\tquery = query.trim();\n\n\tif (query.length === 0) {\n\t\treturn \"\";\n\t}\n\n\t// If already a quoted phrase, escape only interior quotes and preserve phrase syntax\n\tif (query.startsWith('\"') && query.endsWith('\"') && query.length >= 2) {\n\t\tconst inner = query.slice(1, -1);\n\t\treturn `\"${inner.replace(DOUBLE_QUOTE_PATTERN, '\"\"')}\"`;\n\t}\n\n\t// Escape any existing quotes\n\tconst escaped = query.replace(DOUBLE_QUOTE_PATTERN, '\"\"');\n\n\t// If the query contains FTS5 operators (AND, OR, NOT, NEAR),\n\t// pass through with quotes escaped but operators preserved\n\tif (FTS_OPERATORS_PATTERN.test(query)) {\n\t\treturn escaped;\n\t}\n\n\t// For simple queries, wrap each word to handle special chars\n\tconst terms = escaped.split(WHITESPACE_SPLIT_PATTERN).filter((t) => t.length > 0);\n\tif (terms.length === 0) {\n\t\treturn \"\";\n\t}\n\n\t// Join with implicit AND, add prefix matching (*) to all terms\n\t// This allows \"hel wor\" to match \"hello world\"\n\treturn terms.map((t) => `\"${t}\"*`).join(\" \");\n}\n","/**\n * Text Extraction\n *\n * Extracts plain text from Portable Text blocks for FTS indexing.\n * Uses @portabletext/toolkit as base with extensions for custom block types.\n */\n\nimport { toPlainText } from \"@portabletext/toolkit\";\n\nimport type { PortableTextBlock } from \"../content/converters/types.js\";\n\n/**\n * Validate that a value looks like a Portable Text block array.\n * Each element must have at least a `_type` string property.\n */\nfunction isPortableTextArray(value: unknown[]): value is PortableTextBlock[] {\n\treturn value.every(\n\t\t(item) =>\n\t\t\ttypeof item === \"object\" &&\n\t\t\titem !== null &&\n\t\t\t\"_type\" in item &&\n\t\t\ttypeof item._type === \"string\",\n\t);\n}\n\n/**\n * Extract additional text from custom block types that toPlainText doesn't handle\n */\nfunction extractCustomBlockText(block: PortableTextBlock): string {\n\t// Code blocks - include the code content\n\tif (block._type === \"code\" && \"code\" in block && typeof block.code === \"string\") {\n\t\treturn block.code;\n\t}\n\n\t// Image blocks - include alt text and caption\n\tif (block._type === \"image\") {\n\t\tconst parts: string[] = [];\n\t\tif (\"alt\" in block && typeof block.alt === \"string\" && block.alt) {\n\t\t\tparts.push(block.alt);\n\t\t}\n\t\tif (\"caption\" in block && typeof block.caption === \"string\" && block.caption) {\n\t\t\tparts.push(block.caption);\n\t\t}\n\t\treturn parts.join(\" \");\n\t}\n\n\treturn \"\";\n}\n\n/**\n * Extract plain text from Portable Text blocks\n *\n * Uses @portabletext/toolkit's toPlainText for standard blocks,\n * plus extracts text from custom block types (code, images with alt/caption).\n *\n * @param blocks - Array of Portable Text blocks (or a JSON string)\n * @returns Plain text content\n *\n * @example\n * ```typescript\n * const text = extractPlainText([\n * {\n * _type: \"block\",\n * _key: \"abc\",\n * children: [{ _type: \"span\", _key: \"s1\", text: \"Hello World\" }]\n * }\n * ]);\n * // Returns: \"Hello World\"\n * ```\n */\nexport function extractPlainText(blocks: PortableTextBlock[] | string | null | undefined): string {\n\tif (!blocks) {\n\t\treturn \"\";\n\t}\n\n\t// Handle JSON string input\n\tlet parsedBlocks: PortableTextBlock[];\n\tif (typeof blocks === \"string\") {\n\t\ttry {\n\t\t\tparsedBlocks = JSON.parse(blocks);\n\t\t} catch {\n\t\t\t// If it's not valid JSON, treat as plain text\n\t\t\treturn blocks;\n\t\t}\n\t} else {\n\t\tparsedBlocks = blocks;\n\t}\n\n\tif (!Array.isArray(parsedBlocks)) {\n\t\treturn \"\";\n\t}\n\n\t// Use official toPlainText for standard blocks.\n\t// toPlainText expects `{ _type: string; [key: string]: any }[]` but our blocks use\n\t// `unknown` index sigs. They're structurally compatible at runtime — spread each block\n\t// to satisfy the wider index signature without an unsafe cast.\n\tconst toolkitBlocks = parsedBlocks.map((b) => {\n\t\tconst obj: Record<string, unknown> & { _type: string } = { _type: b._type };\n\t\tfor (const [key, val] of Object.entries(b)) {\n\t\t\tobj[key] = val;\n\t\t}\n\t\treturn obj;\n\t});\n\tconst standardText = toPlainText(toolkitBlocks);\n\n\t// Extract text from custom block types that toPlainText doesn't handle\n\tconst customTexts = parsedBlocks.map(extractCustomBlockText).filter((text) => text.length > 0);\n\n\t// Combine both\n\tconst allTexts = [standardText, ...customTexts].filter((t) => t.length > 0);\n\treturn allTexts.join(\"\\n\");\n}\n\n/**\n * Extract searchable text from a content entry\n *\n * Extracts text from specified fields, handling both plain text and Portable Text.\n *\n * @param entry - Content entry data\n * @param fields - Field names to extract text from\n * @returns Object mapping field names to extracted text\n */\nexport function extractSearchableFields(\n\tentry: Record<string, unknown>,\n\tfields: string[],\n): Record<string, string> {\n\tconst result: Record<string, string> = {};\n\n\tfor (const field of fields) {\n\t\tconst value = entry[field];\n\n\t\tif (value === null || value === undefined) {\n\t\t\tresult[field] = \"\";\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (typeof value === \"string\") {\n\t\t\t// Could be plain text or JSON Portable Text\n\t\t\tif (value.startsWith(\"[\")) {\n\t\t\t\tresult[field] = extractPlainText(value);\n\t\t\t} else {\n\t\t\t\tresult[field] = value;\n\t\t\t}\n\t\t} else if (Array.isArray(value)) {\n\t\t\t// Validate the array looks like Portable Text before treating it as such\n\t\t\tif (isPortableTextArray(value)) {\n\t\t\t\tresult[field] = extractPlainText(value);\n\t\t\t} else {\n\t\t\t\tresult[field] = JSON.stringify(value);\n\t\t\t}\n\t\t} else if (typeof value === \"object\") {\n\t\t\t// Object — serialize to JSON for searchable text\n\t\t\tresult[field] = JSON.stringify(value);\n\t\t} else if (typeof value === \"number\" || typeof value === \"boolean\") {\n\t\t\tresult[field] = `${value}`;\n\t\t} else {\n\t\t\tresult[field] = \"\";\n\t\t}\n\t}\n\n\treturn result;\n}\n","/**\n * Search Module\n *\n * Full-text search for EmDash using SQLite FTS5.\n */\n\n// Types\nexport type {\n\tSearchConfig,\n\tSearchOptions,\n\tCollectionSearchOptions,\n\tSearchResult,\n\tSearchResponse,\n\tSuggestOptions,\n\tSuggestion,\n\tSearchStats,\n} from \"./types.js\";\n\n// FTS Manager\nexport { FTSManager } from \"./fts-manager.js\";\n\n// Query functions (public API uses getDb() internally)\nexport { search, searchWithDb, searchCollection, getSuggestions, getSearchStats } from \"./query.js\";\n\n// Text extraction\nexport { extractPlainText, extractSearchableFields } from \"./text-extraction.js\";\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"secrets-CSwQIl4q.mjs","names":[],"sources":["../src/config/secrets.ts"],"sourcesContent":["/**\n * Centralized secrets module\n *\n * Single source of truth for site-level cryptographic secrets:\n *\n * - `EMDASH_ENCRYPTION_KEY` — primary key for encrypting plugin secrets at\n * rest. Multi-key (comma-separated) for rotation forward-compat. v1 ships\n * single-key. Format: `emdash_enc_v1_<43 base64url chars>` representing\n * 32 random bytes. **Operator-provided; never stored in the database.**\n * Losing the key means losing every secret encrypted with it. Validated\n * at runtime startup via `validateEncryptionKeyAtStartup` — request-time\n * resolution does not depend on it, so a malformed key can't 500 the\n * preview/comment hot paths for unrelated visitors.\n * - `EMDASH_IP_SALT` (optional) / DB-stored `emdash:ip_salt` — site-specific\n * salt for hashing commenter IPs. Generated and persisted on first need\n * if no env override is set. Replaces the previous hardcoded\n * `\"emdash-ip-salt\"` constant which was correlatable across installs.\n * - `EMDASH_PREVIEW_SECRET` (optional) / DB-stored `emdash:preview_secret` —\n * HMAC secret for signing preview URLs. Generated and persisted on first\n * need if no env override is set. Replaces the previous empty-string\n * fallback which silently disabled preview-token verification.\n *\n * The `EMDASH_AUTH_SECRET` env var is consulted only as a legacy fallback\n * source for the IP salt — that's the only path the prior code actually\n * read it from. New deployments don't need to set it.\n *\n * Modeled on `resolveS3Config` in `../storage/s3.ts`.\n */\n\nimport { sha256 } from \"@oslojs/crypto/sha2\";\nimport { encodeHexLowerCase } from \"@oslojs/encoding\";\nimport type { Kysely } from \"kysely\";\n\nimport { after } from \"../after.js\";\nimport { OptionsRepository } from \"../database/repositories/options.js\";\nimport type { Database } from \"../database/types.js\";\nimport { decodeBase64url, encodeBase64url } from \"../utils/base64.js\";\nimport {\n\tcreateSingleFlightCache,\n\ttype SingleFlightCache,\n\tsingleFlightCached,\n} from \"../utils/single-flight-cache.js\";\n\n/** v1 encryption key prefix. Bumping requires a separate KDF version. */\nexport const ENCRYPTION_KEY_PREFIX = \"emdash_enc_v1_\";\n\n/** 32 random bytes encoded as unpadded base64url = 43 chars. */\nconst ENCRYPTION_KEY_BODY_LENGTH = 43;\n\nconst REGEX_META_PATTERN = /[.*+?^${}()|[\\]\\\\]/g;\n\n/**\n * Built from the prefix constant via interpolation. The prefix has no regex\n * metacharacters today (`emdash_enc_v1_`), but escaping is cheap defense\n * against anyone changing the prefix in a future bump without remembering.\n */\nconst ENCRYPTION_KEY_PATTERN = new RegExp(\n\t`^${ENCRYPTION_KEY_PREFIX.replace(REGEX_META_PATTERN, \"\\\\$&\")}[A-Za-z0-9_-]{${ENCRYPTION_KEY_BODY_LENGTH}}$`,\n);\n\n/** Options-table key for the persisted commenter-IP salt. */\nexport const IP_SALT_OPTION_KEY = \"emdash:ip_salt\";\n\n/** Options-table key for the persisted preview HMAC secret. */\nexport const PREVIEW_SECRET_OPTION_KEY = \"emdash:preview_secret\";\n\n/** Length in bytes of generated values. 32 bytes = 256 bits. */\nconst GENERATED_SECRET_BYTES = 32;\n\n/**\n * A parsed encryption key with its kid (key id) fingerprint.\n *\n * `kid` is the first 8 chars of the SHA-256 hash of the decoded key bytes\n * (lowercase hex), used to tag envelopes so the decryptor can pick the right\n * key during rotation.\n */\nexport interface ParsedEncryptionKey {\n\t/** 8-char lowercase hex fingerprint derived from the decoded key bytes. */\n\tkid: string;\n\t/** The 32 raw key bytes, ready for `crypto.subtle.importKey`. */\n\tkey: Uint8Array;\n\t/** The original env-var-formatted string (kept for re-emit; never log). */\n\traw: string;\n}\n\n/** Resolved site secrets. */\nexport interface ResolvedSecrets {\n\t/** HMAC secret for preview URLs. Always non-empty after resolution. */\n\tpreviewSecret: string;\n\t/**\n\t * Source of `previewSecret`. Useful for diagnostics; never expose the\n\t * value itself, only the source.\n\t */\n\tpreviewSecretSource: \"env\" | \"db\";\n\t/** Salt for hashing commenter IPs. Always non-empty after resolution. */\n\tipSalt: string;\n\t/** Source of `ipSalt`. */\n\tipSaltSource: \"env\" | \"db\";\n}\n\n/** Inputs for `resolveSecrets`. */\nexport interface ResolveSecretsOptions {\n\t/**\n\t * The Kysely DB used to persist (and read back) generated salt/preview\n\t * secret values. Required — these values must be stable across requests\n\t * within a deployment.\n\t */\n\tdb: Kysely<Database>;\n\t/**\n\t * Optional explicit env override map. When omitted, falls back to\n\t * `import.meta.env` via the global accessor below. Tests pass an\n\t * explicit map to avoid leaking process state.\n\t */\n\tenv?: SecretsEnv;\n\t/**\n\t * @internal Test seam: inject a custom OptionsRepository to exercise\n\t * the lost-race re-read branch. Production callers never set this.\n\t */\n\t_repo?: OptionsRepository;\n}\n\n/** Environment-variable shape consulted by the resolver. */\nexport interface SecretsEnv {\n\t/**\n\t * Read by `validateEncryptionKeyAtStartup` and (in a follow-up PR) by the\n\t * plugin-secret encryption layer. **Not** consulted by `resolveSecrets`,\n\t * so a malformed value can't 500 the preview/comment hot paths.\n\t */\n\tEMDASH_ENCRYPTION_KEY?: string;\n\tEMDASH_PREVIEW_SECRET?: string;\n\t/** Legacy alias; new docs point at EMDASH_PREVIEW_SECRET. */\n\tPREVIEW_SECRET?: string;\n\tEMDASH_IP_SALT?: string;\n\t/**\n\t * Legacy fallback. Prior code derived the IP salt from\n\t * `EMDASH_AUTH_SECRET || AUTH_SECRET || \"emdash-ip-salt\"`. We preserve\n\t * the env-var fallback (so existing installs keep their stable salt)\n\t * but no longer read it from `import.meta.env` in route handlers.\n\t */\n\tEMDASH_AUTH_SECRET?: string;\n\t/** Legacy alias. */\n\tAUTH_SECRET?: string;\n}\n\n/**\n * Class of validation failures raised by this module.\n *\n * Errors here are operator-facing config problems (malformed key, etc.).\n * They are thrown rather than soft-skipped so misconfiguration fails loudly\n * at startup instead of silently degrading at request time.\n */\nexport class EmDashSecretsError extends Error {\n\toverride readonly name = \"EmDashSecretsError\";\n\treadonly code: string;\n\n\tconstructor(message: string, code: string) {\n\t\tsuper(message);\n\t\tthis.code = code;\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Encryption key parsing\n// ---------------------------------------------------------------------------\n\n/**\n * Parse the `EMDASH_ENCRYPTION_KEY` env var.\n *\n * Accepts a single key or a comma-separated list. The first entry is the\n * primary (used for new writes); all entries are tried for decryption,\n * matched by `kid`. Whitespace around commas is tolerated. Empty entries\n * (e.g. trailing comma) are ignored.\n *\n * Returns `null` for an unset/empty input. Throws `EmDashSecretsError` on\n * any malformed entry — silent skipping would mask deployment mistakes.\n */\nexport async function parseEncryptionKeys(\n\traw: string | undefined,\n): Promise<ParsedEncryptionKey[] | null> {\n\tif (!raw) return null;\n\n\tconst entries = raw\n\t\t.split(\",\")\n\t\t.map((entry) => entry.trim())\n\t\t.filter((entry) => entry.length > 0);\n\n\tif (entries.length === 0) return null;\n\n\tconst parsed: ParsedEncryptionKey[] = [];\n\tconst seenKids = new Set<string>();\n\n\tfor (const entry of entries) {\n\t\tif (!ENCRYPTION_KEY_PATTERN.test(entry)) {\n\t\t\tthrow new EmDashSecretsError(\n\t\t\t\t`EMDASH_ENCRYPTION_KEY entry is malformed (expected \"${ENCRYPTION_KEY_PREFIX}\" followed by ${ENCRYPTION_KEY_BODY_LENGTH} base64url chars). Generate one with \\`emdash secrets generate\\`.`,\n\t\t\t\t\"INVALID_ENCRYPTION_KEY\",\n\t\t\t);\n\t\t}\n\n\t\tconst body = entry.slice(ENCRYPTION_KEY_PREFIX.length);\n\t\tconst key = decodeBase64urlStrict(body);\n\t\tif (!key) {\n\t\t\tthrow new EmDashSecretsError(\n\t\t\t\t\"EMDASH_ENCRYPTION_KEY body is not valid base64url\",\n\t\t\t\t\"INVALID_ENCRYPTION_KEY\",\n\t\t\t);\n\t\t}\n\t\tif (key.length !== GENERATED_SECRET_BYTES) {\n\t\t\tthrow new EmDashSecretsError(\n\t\t\t\t`EMDASH_ENCRYPTION_KEY must decode to ${GENERATED_SECRET_BYTES} bytes, got ${key.length}`,\n\t\t\t\t\"INVALID_ENCRYPTION_KEY\",\n\t\t\t);\n\t\t}\n\n\t\t// Reject non-canonical base64url. 43 chars decode to 32 bytes but\n\t\t// the last char only carries 2 information bits — multiple raw\n\t\t// strings can decode to the same bytes. Forcing canonical form\n\t\t// guarantees `kid` (derived from bytes) is stable per key\n\t\t// material, regardless of how the operator pasted it.\n\t\tconst canonical = encodeBase64url(key);\n\t\tif (canonical !== body) {\n\t\t\tthrow new EmDashSecretsError(\n\t\t\t\t\"EMDASH_ENCRYPTION_KEY body is not canonical base64url. Generate one with `emdash secrets generate`.\",\n\t\t\t\t\"INVALID_ENCRYPTION_KEY\",\n\t\t\t);\n\t\t}\n\n\t\tconst kid = fingerprintKeyBytes(key);\n\t\tif (seenKids.has(kid)) {\n\t\t\t// Duplicate keys are user error (paste mistake during rotation).\n\t\t\t// We dedupe rather than throw — the rotation flow is forgiving.\n\t\t\tcontinue;\n\t\t}\n\t\tseenKids.add(kid);\n\t\tparsed.push({ kid, key, raw: entry });\n\t}\n\n\t// `parsed` always has at least one entry here: `entries` was non-empty\n\t// after filtering, the loop runs at least once, the first iteration\n\t// always passes the empty-`seenKids` check.\n\treturn parsed;\n}\n\n/**\n * Compute the kid for a raw key string (the env-var form including the\n * `emdash_enc_v1_` prefix). Public so the CLI's `fingerprint` subcommand\n * and admin endpoints can show kids without exposing raw keys.\n *\n * The kid is derived from the decoded key **bytes**, not the raw string,\n * so admin endpoints / future rotation flows can match envelope kids\n * against bytes regardless of how the env var was originally spelled.\n *\n * Validates the same shape as `parseEncryptionKeys` — including canonical\n * base64url — so the CLI can't print a kid for a key the runtime would\n * later refuse to load.\n *\n * Throws `EmDashSecretsError` for malformed or non-canonical input.\n */\nexport async function fingerprintKey(raw: string): Promise<string> {\n\tif (!ENCRYPTION_KEY_PATTERN.test(raw)) {\n\t\tthrow new EmDashSecretsError(\n\t\t\t`Key must match \"${ENCRYPTION_KEY_PREFIX}\" followed by ${ENCRYPTION_KEY_BODY_LENGTH} base64url chars`,\n\t\t\t\"INVALID_ENCRYPTION_KEY\",\n\t\t);\n\t}\n\tconst body = raw.slice(ENCRYPTION_KEY_PREFIX.length);\n\tconst bytes = decodeBase64urlStrict(body);\n\tif (!bytes || bytes.length !== GENERATED_SECRET_BYTES || encodeBase64url(bytes) !== body) {\n\t\tthrow new EmDashSecretsError(\n\t\t\t`Key body must decode to ${GENERATED_SECRET_BYTES} canonical base64url bytes`,\n\t\t\t\"INVALID_ENCRYPTION_KEY\",\n\t\t);\n\t}\n\treturn fingerprintKeyBytes(bytes);\n}\n\n/**\n * Internal: kid derivation from raw key bytes. The single source of truth\n * for what makes two keys \"the same key\" — used by both `parseEncryptionKeys`\n * and `fingerprintKey`.\n */\nfunction fingerprintKeyBytes(key: Uint8Array): string {\n\treturn encodeHexLowerCase(sha256(key)).slice(0, 8);\n}\n\n/**\n * Generate a fresh `EMDASH_ENCRYPTION_KEY` value. Used by the CLI's\n * `secrets generate` subcommand and by `create-emdash` scaffolding.\n */\nexport function generateEncryptionKey(): string {\n\tconst bytes = new Uint8Array(GENERATED_SECRET_BYTES);\n\tcrypto.getRandomValues(bytes);\n\treturn `${ENCRYPTION_KEY_PREFIX}${encodeBase64url(bytes)}`;\n}\n\n// ---------------------------------------------------------------------------\n// Site-secret resolution (DB-backed with env override)\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve site secrets. Reads env vars; for IP salt and preview secret,\n * falls back to a DB-stored value, generating one atomically on first need.\n *\n * Idempotent. Concurrent callers race on the atomic `setIfAbsent`; whichever\n * wins, all callers converge on the same stored value.\n *\n * Note: `EMDASH_ENCRYPTION_KEY` is **not** consumed here. It's validated\n * separately at runtime startup (see `validateEncryptionKeyAtStartup`) so a\n * malformed key can't take down preview-token verification or comment\n * submission for unrelated visitors. Future plugin-secret encryption code\n * will read it via its own dedicated helper.\n */\nexport async function resolveSecrets(options: ResolveSecretsOptions): Promise<ResolvedSecrets> {\n\tconst env = options.env ?? readDefaultEnv();\n\tconst repo = options._repo ?? new OptionsRepository(options.db);\n\n\tconst previewEnvOverride = pickFirstNonEmpty(env.EMDASH_PREVIEW_SECRET, env.PREVIEW_SECRET);\n\tconst ipSaltEnvOverride = pickFirstNonEmpty(\n\t\tenv.EMDASH_IP_SALT,\n\t\tenv.EMDASH_AUTH_SECRET,\n\t\tenv.AUTH_SECRET,\n\t);\n\n\tconst [previewSecret, ipSalt] = await Promise.all([\n\t\tpreviewEnvOverride !== null\n\t\t\t? Promise.resolve({ value: previewEnvOverride, source: \"env\" as const })\n\t\t\t: ensureGeneratedOption(repo, PREVIEW_SECRET_OPTION_KEY),\n\t\tipSaltEnvOverride !== null\n\t\t\t? Promise.resolve({ value: ipSaltEnvOverride, source: \"env\" as const })\n\t\t\t: ensureGeneratedOption(repo, IP_SALT_OPTION_KEY),\n\t]);\n\n\treturn {\n\t\tpreviewSecret: previewSecret.value,\n\t\tpreviewSecretSource: previewSecret.source,\n\t\tipSalt: ipSalt.value,\n\t\tipSaltSource: ipSalt.source,\n\t};\n}\n\n/**\n * Validate `EMDASH_ENCRYPTION_KEY` once at runtime startup. Logs an\n * operator-facing error if the value is malformed but does **not** throw —\n * the key is currently inert (no consumers), and the follow-up PR that\n * actually uses it will throw at point of use. This way, deployment\n * mistakes surface immediately in startup logs without wedging unrelated\n * request paths in the meantime.\n *\n * Returns `true` if the key is unset or valid, `false` if it was malformed.\n */\nexport async function validateEncryptionKeyAtStartup(env?: SecretsEnv): Promise<boolean> {\n\tconst resolved = env ?? readDefaultEnv();\n\ttry {\n\t\tawait parseEncryptionKeys(resolved.EMDASH_ENCRYPTION_KEY);\n\t\treturn true;\n\t} catch (error) {\n\t\tif (error instanceof EmDashSecretsError) {\n\t\t\tconsole.error(\n\t\t\t\t`[emdash] EMDASH_ENCRYPTION_KEY is invalid: ${error.message} ` +\n\t\t\t\t\t\"Plugin-secret encryption will fail once it ships. \" +\n\t\t\t\t\t\"Generate a fresh key with `emdash secrets generate`.\",\n\t\t\t);\n\t\t\treturn false;\n\t\t}\n\t\tthrow error;\n\t}\n}\n\n/**\n * Per-DB cache of resolved secrets, keyed by Kysely instance identity.\n *\n * The resolved values are stable for the lifetime of the deployment (env\n * vars don't change without a restart, and DB-stored values are written\n * once via `setIfAbsent`). Caching avoids one options-table read per\n * request on the hot paths (preview verification, comment hashing).\n *\n * Lives on `globalThis` so module-duplication during SSR bundling can't\n * fragment the cache. See `request-context.ts` for the same pattern.\n *\n * Each db gets its own poison-immune single-flight cache (see\n * `utils/single-flight-cache.ts`): the resolved *value* is cached, never an\n * in-flight promise, so a request cancelled mid-resolve can't strand later\n * preview/comment requests on the isolate.\n */\n// Versioned to prevent cache fragmentation if `ResolvedSecrets`'s shape\n// ever changes. Bump the suffix on incompatible changes so a co-resident\n// older build doesn't read a newer-shape value. Bumped to @2 when the cached\n// value changed from a bare promise to a single-flight cache.\nconst SECRETS_CACHE_KEY = Symbol.for(\"@emdash-cms/core/secrets-cache@2\");\n\ninterface SecretsCacheHolder {\n\tcache: WeakMap<Kysely<Database>, SingleFlightCache<ResolvedSecrets>>;\n}\n\nfunction getSecretsCache(): WeakMap<Kysely<Database>, SingleFlightCache<ResolvedSecrets>> {\n\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- globalThis singleton pattern\n\tconst holder = globalThis as Record<symbol, SecretsCacheHolder | undefined>;\n\tlet entry = holder[SECRETS_CACHE_KEY];\n\tif (!entry) {\n\t\tentry = { cache: new WeakMap() };\n\t\tholder[SECRETS_CACHE_KEY] = entry;\n\t}\n\treturn entry.cache;\n}\n\n/**\n * Memoized wrapper around `resolveSecrets`. Use this from request-time hot\n * paths (preview verification, comment IP hashing) so they don't reread\n * env / re-query options on every request.\n *\n * The cache is keyed by `Kysely` instance, so playground / per-DO / per-test\n * databases each get their own resolution. Concurrent cold callers coalesce\n * onto one resolution via the single-flight lock; a failed resolution\n * propagates to the caller and releases the lock so the next caller retries.\n */\nexport function resolveSecretsCached(db: Kysely<Database>): Promise<ResolvedSecrets> {\n\tconst caches = getSecretsCache();\n\tlet cache = caches.get(db);\n\tif (!cache) {\n\t\tcache = createSingleFlightCache<ResolvedSecrets>();\n\t\tcaches.set(db, cache);\n\t}\n\treturn singleFlightCached(cache, () => resolveSecrets({ db }), {\n\t\tanchor: (promise) => after(() => promise),\n\t\townerTimeoutMs: 30_000,\n\t});\n}\n\n/**\n * Test-only helper: clear the secrets cache. Tests that mutate env between\n * cases need this so a stale resolution doesn't leak across cases.\n *\n * @internal\n */\nexport function _clearSecretsCacheForTesting(): void {\n\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- globalThis singleton pattern\n\tconst holder = globalThis as Record<symbol, SecretsCacheHolder | undefined>;\n\tholder[SECRETS_CACHE_KEY] = undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Internals\n// ---------------------------------------------------------------------------\n\n/**\n * Read or generate-and-persist a random base64url secret stored in the\n * options table.\n *\n * Concurrency: `setIfAbsent` is an atomic INSERT...ON CONFLICT DO NOTHING.\n * On race, the loser re-reads to converge on the winner's value.\n */\nasync function ensureGeneratedOption(\n\trepo: OptionsRepository,\n\toptionKey: string,\n): Promise<{ value: string; source: \"db\" }> {\n\tconst existing = await repo.get<string>(optionKey);\n\tif (typeof existing === \"string\" && existing.length > 0) {\n\t\treturn { value: existing, source: \"db\" };\n\t}\n\n\tconst generated = generateRandomSecret();\n\tconst inserted = await repo.setIfAbsent(optionKey, generated);\n\tif (inserted) {\n\t\treturn { value: generated, source: \"db\" };\n\t}\n\n\t// Lost the race — another process inserted first. Re-read to pick up\n\t// the winner. If the row is somehow still missing or empty, treat that\n\t// as a real error rather than looping.\n\tconst winner = await repo.get<string>(optionKey);\n\tif (typeof winner !== \"string\" || winner.length === 0) {\n\t\tthrow new EmDashSecretsError(\n\t\t\t`Failed to persist generated secret for \"${optionKey}\"`,\n\t\t\t\"SECRET_PERSIST_FAILED\",\n\t\t);\n\t}\n\treturn { value: winner, source: \"db\" };\n}\n\n/** Generate 32 random bytes encoded as unpadded base64url. */\nfunction generateRandomSecret(): string {\n\tconst bytes = new Uint8Array(GENERATED_SECRET_BYTES);\n\tcrypto.getRandomValues(bytes);\n\treturn encodeBase64url(bytes);\n}\n\n/** Return the first non-empty string from `values`, or `null` if all are empty. */\nfunction pickFirstNonEmpty(...values: (string | undefined)[]): string | null {\n\tfor (const value of values) {\n\t\tif (typeof value === \"string\" && value.length > 0) {\n\t\t\treturn value;\n\t\t}\n\t}\n\treturn null;\n}\n\nconst BASE64URL_CHARSET_PATTERN = /^[A-Za-z0-9_-]+$/;\n\n/**\n * Validate base64url shape and decode. Returns `null` on malformed input\n * (rather than throwing) so the caller can produce a config-specific error.\n */\nfunction decodeBase64urlStrict(input: string): Uint8Array | null {\n\t// `decodeBase64url` accepts padded input too; the env-var format is\n\t// strictly unpadded base64url, so we do a charset check first.\n\tif (!BASE64URL_CHARSET_PATTERN.test(input)) return null;\n\ttry {\n\t\treturn decodeBase64url(input);\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Default env reader.\n *\n * Note: this is the **only** code path in core that reads both\n * `import.meta.env` and `process.env`. Route handlers should not — they\n * always run inside the Astro/Vite bundle where `import.meta.env` is\n * the correct source. This resolver is shared with the CLI surface (via\n * `cli/commands/secrets.ts`) which runs outside the bundle, so we\n * deliberately consult both. `import.meta.env` wins so build-time\n * substitutions are honored when present.\n *\n * The convention documented in AGENTS.md (\"import.meta.env.EMDASH_X ||\n * import.meta.env.X\") is the route-handler convention; this is the\n * shared-with-CLI exception.\n */\nfunction readDefaultEnv(): SecretsEnv {\n\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- import.meta.env is loose by design\n\tconst meta = (import.meta.env ?? {}) as Record<string, string | undefined>;\n\tconst proc = typeof process !== \"undefined\" && process.env ? process.env : {};\n\n\treturn {\n\t\tEMDASH_ENCRYPTION_KEY: meta.EMDASH_ENCRYPTION_KEY ?? proc.EMDASH_ENCRYPTION_KEY,\n\t\tEMDASH_PREVIEW_SECRET: meta.EMDASH_PREVIEW_SECRET ?? proc.EMDASH_PREVIEW_SECRET,\n\t\tPREVIEW_SECRET: meta.PREVIEW_SECRET ?? proc.PREVIEW_SECRET,\n\t\tEMDASH_IP_SALT: meta.EMDASH_IP_SALT ?? proc.EMDASH_IP_SALT,\n\t\tEMDASH_AUTH_SECRET: meta.EMDASH_AUTH_SECRET ?? proc.EMDASH_AUTH_SECRET,\n\t\tAUTH_SECRET: meta.AUTH_SECRET ?? proc.AUTH_SECRET,\n\t};\n}\n"],"mappings":"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"}
|