emdash 0.34.0 → 0.35.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/{adapters-34EN2eXO.d.mts → adapters-Bb1vG41D.d.mts} +8 -1
- package/dist/adapters-Bb1vG41D.d.mts.map +1 -0
- package/dist/api/route-utils.d.mts +3 -3
- package/dist/api/route-utils.mjs +9 -9
- package/dist/api/schemas/index.d.mts +1 -1
- package/dist/api/schemas/index.mjs +1 -1
- package/dist/{api-DdAsirBe.mjs → api-BeBglVxU.mjs} +10 -10
- package/dist/{api-DdAsirBe.mjs.map → api-BeBglVxU.mjs.map} +1 -1
- package/dist/{api-tokens-CFXu8F_2.mjs → api-tokens-KO24G1Fd.mjs} +2 -2
- package/dist/{api-tokens-CFXu8F_2.mjs.map → api-tokens-KO24G1Fd.mjs.map} +1 -1
- package/dist/{apply-D5zfeD09.mjs → apply-kUDt80-G.mjs} +16 -16
- package/dist/{apply-D5zfeD09.mjs.map → apply-kUDt80-G.mjs.map} +1 -1
- package/dist/astro/index.d.mts +12 -12
- package/dist/astro/index.d.mts.map +1 -1
- package/dist/astro/index.mjs +71 -15
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +10 -10
- package/dist/astro/middleware/auth.mjs +4 -4
- package/dist/astro/middleware/redirect.mjs +2 -2
- package/dist/astro/middleware/request-context.mjs +1 -1
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.d.mts +10 -10
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +143 -74
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +1 -1
- package/dist/astro/routes/api/admin/allowed-domains/index.mjs +1 -1
- package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +2 -2
- package/dist/astro/routes/api/admin/api-tokens/index.mjs +2 -2
- package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +2 -2
- package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +3 -3
- package/dist/astro/routes/api/admin/byline-fields/index.mjs +3 -3
- package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +3 -3
- package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +7 -7
- package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +7 -7
- package/dist/astro/routes/api/admin/bylines/index.mjs +7 -7
- package/dist/astro/routes/api/admin/comments/_id_/status.mjs +5 -5
- package/dist/astro/routes/api/admin/comments/_id_.mjs +2 -2
- package/dist/astro/routes/api/admin/comments/bulk.mjs +3 -3
- package/dist/astro/routes/api/admin/comments/counts.mjs +2 -2
- package/dist/astro/routes/api/admin/comments/index.mjs +3 -3
- package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +1 -1
- package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +1 -1
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.mjs +7 -7
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.mjs +7 -7
- package/dist/astro/routes/api/admin/media-usage/repair.mjs +4 -4
- package/dist/astro/routes/api/admin/media-usage/work/index.mjs +7 -7
- package/dist/astro/routes/api/admin/media-usage/work/retry.mjs +7 -7
- package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +1 -1
- package/dist/astro/routes/api/admin/oauth-clients/index.mjs +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +28 -28
- package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +28 -28
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +28 -28
- package/dist/astro/routes/api/admin/plugins/_id_/mcp.mjs +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs +28 -28
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +28 -28
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +28 -28
- package/dist/astro/routes/api/admin/plugins/index.mjs +28 -28
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +28 -28
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +28 -28
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +28 -28
- package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +28 -28
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +29 -29
- package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +28 -28
- package/dist/astro/routes/api/admin/plugins/registry/install.mjs +29 -29
- package/dist/astro/routes/api/admin/plugins/updates.mjs +28 -28
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +28 -28
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +1 -1
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +28 -28
- package/dist/astro/routes/api/admin/users/_id_/index.mjs +1 -1
- package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +1 -1
- package/dist/astro/routes/api/admin/users/index.mjs +1 -1
- package/dist/astro/routes/api/auth/dev-bypass.d.mts.map +1 -1
- package/dist/astro/routes/api/auth/dev-bypass.mjs +2 -6
- package/dist/astro/routes/api/auth/dev-bypass.mjs.map +1 -1
- package/dist/astro/routes/api/auth/invite/complete.mjs +3 -3
- package/dist/astro/routes/api/auth/invite/index.mjs +2 -2
- package/dist/astro/routes/api/auth/invite/register-options.mjs +3 -3
- package/dist/astro/routes/api/auth/logout.mjs +1 -1
- package/dist/astro/routes/api/auth/magic-link/send.mjs +3 -3
- package/dist/astro/routes/api/auth/magic-link/verify.mjs +1 -1
- package/dist/astro/routes/api/auth/me.mjs +2 -2
- package/dist/astro/routes/api/auth/mode.mjs +1 -1
- package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs +2 -2
- package/dist/astro/routes/api/auth/oauth/_provider_.mjs +1 -1
- package/dist/astro/routes/api/auth/passkey/_id_.mjs +1 -1
- package/dist/astro/routes/api/auth/passkey/options.mjs +4 -4
- package/dist/astro/routes/api/auth/passkey/register/options.mjs +3 -3
- package/dist/astro/routes/api/auth/passkey/register/verify.mjs +3 -3
- package/dist/astro/routes/api/auth/passkey/verify.mjs +3 -3
- package/dist/astro/routes/api/auth/signup/complete.mjs +3 -3
- package/dist/astro/routes/api/auth/signup/request.mjs +3 -3
- package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +5 -5
- package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs +4 -4
- package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +2 -2
- package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs +9 -9
- package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs +9 -9
- package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +2 -2
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +6 -6
- package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_.mjs +2 -2
- package/dist/astro/routes/api/content/_collection_/authors.mjs +1 -1
- package/dist/astro/routes/api/content/_collection_/index.mjs +2 -2
- package/dist/astro/routes/api/content/_collection_/trash.mjs +2 -2
- package/dist/astro/routes/api/dashboard.mjs +4 -4
- package/dist/astro/routes/api/import/probe.d.mts +3 -3
- package/dist/astro/routes/api/import/probe.mjs +5 -5
- package/dist/astro/routes/api/import/wordpress/analyze.mjs +1 -1
- package/dist/astro/routes/api/import/wordpress/execute.d.mts +11 -11
- package/dist/astro/routes/api/import/wordpress/execute.mjs +7 -7
- package/dist/astro/routes/api/import/wordpress/media.d.mts +10 -10
- package/dist/astro/routes/api/import/wordpress/media.mjs +2 -2
- package/dist/astro/routes/api/import/wordpress/prepare.mjs +4 -4
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts +11 -11
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +4 -4
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.d.mts +1 -1
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +5 -5
- package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +11 -11
- package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +11 -11
- package/dist/astro/routes/api/manifest.mjs +2 -2
- package/dist/astro/routes/api/mcp.mjs +38 -38
- package/dist/astro/routes/api/media/_id_/confirm.mjs +2 -2
- package/dist/astro/routes/api/media/_id_/upload.mjs +2 -2
- package/dist/astro/routes/api/media/_id_/usage.mjs +6 -6
- package/dist/astro/routes/api/media/_id_.mjs +5 -5
- package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +1 -1
- package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +1 -1
- package/dist/astro/routes/api/media/providers/index.mjs +1 -1
- package/dist/astro/routes/api/media/upload-url.mjs +3 -3
- package/dist/astro/routes/api/media.mjs +5 -5
- package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +3 -3
- package/dist/astro/routes/api/menus/_name_/items.mjs +3 -3
- package/dist/astro/routes/api/menus/_name_/reorder.mjs +3 -3
- package/dist/astro/routes/api/menus/_name_/translations.mjs +3 -3
- package/dist/astro/routes/api/menus/_name_.mjs +3 -3
- package/dist/astro/routes/api/menus/index.mjs +3 -3
- package/dist/astro/routes/api/oauth/authorize.mjs +5 -5
- package/dist/astro/routes/api/oauth/device/authorize.mjs +3 -3
- package/dist/astro/routes/api/oauth/device/code.mjs +4 -4
- package/dist/astro/routes/api/oauth/device/token.mjs +4 -4
- package/dist/astro/routes/api/oauth/register.mjs +1 -1
- 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 +4 -4
- package/dist/astro/routes/api/openapi.json.mjs +1 -1
- package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +3 -3
- package/dist/astro/routes/api/redirects/404s/index.mjs +3 -3
- package/dist/astro/routes/api/redirects/404s/summary.mjs +3 -3
- package/dist/astro/routes/api/redirects/_id_.mjs +4 -4
- package/dist/astro/routes/api/redirects/index.mjs +4 -4
- package/dist/astro/routes/api/relations/_id_/index.mjs +9 -9
- package/dist/astro/routes/api/relations/_id_/translations.mjs +8 -8
- package/dist/astro/routes/api/relations/index.mjs +9 -9
- package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +1 -1
- package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +1 -1
- package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +28 -28
- package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +28 -28
- package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +28 -28
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +28 -28
- package/dist/astro/routes/api/schema/collections/index.mjs +28 -28
- package/dist/astro/routes/api/schema/collections/reorder.mjs +28 -28
- package/dist/astro/routes/api/schema/index.mjs +6 -6
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs +28 -28
- package/dist/astro/routes/api/schema/orphans/index.mjs +28 -28
- package/dist/astro/routes/api/search/enable.mjs +5 -5
- package/dist/astro/routes/api/search/index.mjs +4 -4
- package/dist/astro/routes/api/search/rebuild.mjs +5 -5
- package/dist/astro/routes/api/search/stats.mjs +4 -4
- package/dist/astro/routes/api/search/suggest.mjs +4 -4
- package/dist/astro/routes/api/sections/_slug_.mjs +4 -4
- package/dist/astro/routes/api/sections/index.mjs +4 -4
- package/dist/astro/routes/api/settings/backups/archives/_name_.mjs +3 -3
- package/dist/astro/routes/api/settings/backups/archives/index.mjs +3 -3
- package/dist/astro/routes/api/settings/backups/export.mjs +3 -3
- package/dist/astro/routes/api/settings/backups/index.mjs +3 -3
- package/dist/astro/routes/api/settings/email.mjs +1 -1
- package/dist/astro/routes/api/settings.mjs +5 -5
- package/dist/astro/routes/api/setup/admin-verify.mjs +4 -4
- package/dist/astro/routes/api/setup/admin.mjs +4 -4
- package/dist/astro/routes/api/setup/dev-bypass.d.mts.map +1 -1
- package/dist/astro/routes/api/setup/dev-bypass.mjs +18 -21
- package/dist/astro/routes/api/setup/dev-bypass.mjs.map +1 -1
- package/dist/astro/routes/api/setup/index.d.mts.map +1 -1
- package/dist/astro/routes/api/setup/index.mjs +15 -21
- package/dist/astro/routes/api/setup/index.mjs.map +1 -1
- package/dist/astro/routes/api/setup/status.mjs +2 -2
- package/dist/astro/routes/api/snapshot.mjs +4 -4
- package/dist/astro/routes/api/taxonomies/_name_/reorder.mjs +6 -6
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +6 -6
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +6 -6
- package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +6 -6
- package/dist/astro/routes/api/taxonomies/_name_/translations.mjs +6 -6
- package/dist/astro/routes/api/taxonomies/_name_.mjs +6 -6
- package/dist/astro/routes/api/taxonomies/index.mjs +6 -6
- package/dist/astro/routes/api/themes/preview.mjs +2 -2
- package/dist/astro/routes/api/typegen.mjs +3 -3
- package/dist/astro/routes/api/well-known/auth.mjs +1 -1
- package/dist/astro/routes/api/well-known/oauth-authorization-server.mjs +1 -1
- package/dist/astro/routes/api/well-known/oauth-protected-resource.mjs +1 -1
- package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +2 -2
- package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +4 -4
- package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +4 -4
- package/dist/astro/routes/api/widget-areas/_name_.mjs +3 -3
- package/dist/astro/routes/api/widget-areas/index.mjs +4 -4
- package/dist/astro/routes/robots.txt.mjs +2 -2
- package/dist/astro/routes/sitemap-_collection_.xml.mjs +2 -2
- package/dist/astro/routes/sitemap.xml.mjs +2 -2
- package/dist/astro/types.d.mts +14 -14
- package/dist/auth/providers/github.d.mts +1 -1
- package/dist/auth/providers/google.d.mts +1 -1
- package/dist/{authorize-1iFJzPaZ.mjs → authorize-DUElxgOQ.mjs} +1 -1
- package/dist/{authorize-1iFJzPaZ.mjs.map → authorize-DUElxgOQ.mjs.map} +1 -1
- package/dist/{backup-LHjIAVNP.mjs → backup-DWYpJlLj.mjs} +3 -3
- package/dist/{backup-LHjIAVNP.mjs.map → backup-DWYpJlLj.mjs.map} +1 -1
- package/dist/{bundle-CBEXrBnb.mjs → bundle-vn56sLos.mjs} +5 -5
- package/dist/{bundle-CBEXrBnb.mjs.map → bundle-vn56sLos.mjs.map} +1 -1
- package/dist/{byline-fields-C5S8wygO.mjs → byline-fields-txaLGoME.mjs} +1 -1
- package/dist/{byline-fields-C5S8wygO.mjs.map → byline-fields-txaLGoME.mjs.map} +1 -1
- package/dist/{byline-pEcoDps8.mjs → byline-yjvWq4ip.mjs} +4 -4
- package/dist/{byline-pEcoDps8.mjs.map → byline-yjvWq4ip.mjs.map} +1 -1
- package/dist/{bylines-DJfSfXCL.mjs → bylines-Bu7pchQ4.mjs} +3 -3
- package/dist/{bylines-DJfSfXCL.mjs.map → bylines-Bu7pchQ4.mjs.map} +1 -1
- package/dist/{bylines-BWemdX2k.mjs → bylines-DjXS5p0P.mjs} +5 -5
- package/dist/{bylines-BWemdX2k.mjs.map → bylines-DjXS5p0P.mjs.map} +1 -1
- package/dist/{cache-CBJaDy_E.mjs → cache-C46-3Irl.mjs} +2 -2
- package/dist/{cache-CBJaDy_E.mjs.map → cache-C46-3Irl.mjs.map} +1 -1
- package/dist/{challenge-store-CZyFVaad.mjs → challenge-store-SIL5DsF2.mjs} +1 -1
- package/dist/{challenge-store-CZyFVaad.mjs.map → challenge-store-SIL5DsF2.mjs.map} +1 -1
- package/dist/{chunks-DfPax1I0.mjs → chunks-fiZ8G9RJ.mjs} +2 -2
- package/dist/{chunks-DfPax1I0.mjs.map → chunks-fiZ8G9RJ.mjs.map} +1 -1
- package/dist/cli/index.mjs +551 -51
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/cf-access.d.mts +1 -1
- package/dist/client/index.d.mts +2 -2
- package/dist/client/index.mjs +2 -2
- package/dist/{collection-deletion-C8BugSGz.mjs → collection-deletion-DQPvMD8R.mjs} +2 -2
- package/dist/{collection-deletion-C8BugSGz.mjs.map → collection-deletion-DQPvMD8R.mjs.map} +1 -1
- package/dist/{comment-reaction-D6qfHKBc.mjs → comment-reaction-D-2J84UE.mjs} +2 -2
- package/dist/{comment-reaction-D6qfHKBc.mjs.map → comment-reaction-D-2J84UE.mjs.map} +1 -1
- package/dist/{comments-CTOuhTSX.mjs → comments-CTfZtLmM.mjs} +1 -1
- package/dist/{comments-CTOuhTSX.mjs.map → comments-CTfZtLmM.mjs.map} +1 -1
- package/dist/config-COQzTe5Q.d.mts +34 -0
- package/dist/config-COQzTe5Q.d.mts.map +1 -0
- package/dist/{content-BNfwh7xX.mjs → content-ZHCc1ngN.mjs} +3 -3
- package/dist/{content-BNfwh7xX.mjs.map → content-ZHCc1ngN.mjs.map} +1 -1
- package/dist/{content-list-query-DhQ84eVV.d.mts → content-list-query-wJ2WN8Vp.d.mts} +1 -1
- package/dist/{content-list-query-DhQ84eVV.d.mts.map → content-list-query-wJ2WN8Vp.d.mts.map} +1 -1
- package/dist/{content-refresh-DNERP4-f.mjs → content-refresh-B_8SJzuU.mjs} +2 -2
- package/dist/{content-refresh-DNERP4-f.mjs.map → content-refresh-B_8SJzuU.mjs.map} +1 -1
- package/dist/{context-DRgOvSnW.mjs → context-ZldbZcle.mjs} +7 -7
- package/dist/{context-DRgOvSnW.mjs.map → context-ZldbZcle.mjs.map} +1 -1
- package/dist/{dashboard-CB8J89rJ.mjs → dashboard-DvHbk_cv.mjs} +3 -3
- package/dist/{dashboard-CB8J89rJ.mjs.map → dashboard-DvHbk_cv.mjs.map} +1 -1
- package/dist/db/index.d.mts +4 -4
- package/dist/db/index.mjs +30 -7
- package/dist/db/index.mjs.map +1 -1
- package/dist/db/libsql-migrations.d.mts +11 -0
- package/dist/db/libsql-migrations.d.mts.map +1 -0
- package/dist/db/libsql-migrations.mjs +57 -0
- package/dist/db/libsql-migrations.mjs.map +1 -0
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/postgres-migrations.d.mts +10 -0
- package/dist/db/postgres-migrations.d.mts.map +1 -0
- package/dist/db/postgres-migrations.mjs +47 -0
- package/dist/db/postgres-migrations.mjs.map +1 -0
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite-migrations.d.mts +10 -0
- package/dist/db/sqlite-migrations.d.mts.map +1 -0
- package/dist/db/sqlite-migrations.mjs +28 -0
- package/dist/db/sqlite-migrations.mjs.map +1 -0
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/{deferred-tasks-BMsa9F64.d.mts → deferred-tasks-SDWxrgzJ.d.mts} +1 -1
- package/dist/{deferred-tasks-BMsa9F64.d.mts.map → deferred-tasks-SDWxrgzJ.d.mts.map} +1 -1
- package/dist/{device-flow-CaioYGK3.mjs → device-flow-C0GdD_z8.mjs} +4 -4
- package/dist/{device-flow-CaioYGK3.mjs.map → device-flow-C0GdD_z8.mjs.map} +1 -1
- package/dist/direct-executor-CDXeJCSs.mjs +85 -0
- package/dist/direct-executor-CDXeJCSs.mjs.map +1 -0
- package/dist/{escape-C_aLKP3H.mjs → escape-DUY2r2Zs.mjs} +1 -1
- package/dist/{escape-C_aLKP3H.mjs.map → escape-DUY2r2Zs.mjs.map} +1 -1
- package/dist/{field-defs-cache-hg6QkjV9.mjs → field-defs-cache-BzwBlJrR.mjs} +2 -2
- package/dist/{field-defs-cache-hg6QkjV9.mjs.map → field-defs-cache-BzwBlJrR.mjs.map} +1 -1
- package/dist/{fts-manager-CZ-5dVwJ.mjs → fts-manager-DyZEohZC.mjs} +2 -2
- package/dist/{fts-manager-CZ-5dVwJ.mjs.map → fts-manager-DyZEohZC.mjs.map} +1 -1
- package/dist/{hreflang-DNswoYsi.mjs → hreflang-BBe83kKa.mjs} +5 -5
- package/dist/{hreflang-DNswoYsi.mjs.map → hreflang-BBe83kKa.mjs.map} +1 -1
- package/dist/{hreflang-D3Uehfv2.d.mts → hreflang-DwT2E0Sa.d.mts} +2 -2
- package/dist/{hreflang-D3Uehfv2.d.mts.map → hreflang-DwT2E0Sa.d.mts.map} +1 -1
- package/dist/identity-Ys6Dlc9C.mjs +35 -0
- package/dist/identity-Ys6Dlc9C.mjs.map +1 -0
- package/dist/{import-B41u8Rot.mjs → import-DPkLmeXO.mjs} +2 -2
- package/dist/{import-B41u8Rot.mjs.map → import-DPkLmeXO.mjs.map} +1 -1
- package/dist/{index-C3Y-rsyN.d.mts → index-BVBZj4-7.d.mts} +28 -49
- package/dist/index-BVBZj4-7.d.mts.map +1 -0
- package/dist/{index-Cof4-Fab.d.mts → index-C9KuQyO-.d.mts} +3 -3
- package/dist/{index-Cof4-Fab.d.mts.map → index-C9KuQyO-.d.mts.map} +1 -1
- package/dist/index.d.mts +21 -20
- package/dist/index.mjs +42 -42
- package/dist/{load-DmvJrtxS.mjs → load-YJ2xl3ra.mjs} +2 -2
- package/dist/{load-DmvJrtxS.mjs.map → load-YJ2xl3ra.mjs.map} +1 -1
- package/dist/{loader-DLBqRiGP.mjs → loader-CdG_Ci3H.mjs} +2 -2
- package/dist/{loader-DLBqRiGP.mjs.map → loader-CdG_Ci3H.mjs.map} +1 -1
- package/dist/manifest-builder-B252yQUf.mjs +189 -0
- package/dist/manifest-builder-B252yQUf.mjs.map +1 -0
- package/dist/media/index.d.mts +1 -1
- package/dist/media/local-runtime.d.mts +12 -12
- package/dist/media/local-runtime.mjs +2 -2
- package/dist/{media-allowlist-BD6HhgXA.mjs → media-allowlist-D6x-Us02.mjs} +1 -1
- package/dist/{media-allowlist-BD6HhgXA.mjs.map → media-allowlist-D6x-Us02.mjs.map} +1 -1
- package/dist/{media-upload-9yas9zGH.mjs → media-upload-CHBVn6m-.mjs} +2 -2
- package/dist/{media-upload-9yas9zGH.mjs.map → media-upload-CHBVn6m-.mjs.map} +1 -1
- package/dist/{media-usage-De3qvwjc.mjs → media-usage-BZCydfvu.mjs} +4 -4
- package/dist/{media-usage-De3qvwjc.mjs.map → media-usage-BZCydfvu.mjs.map} +1 -1
- package/dist/{media-usage-work-B9Bksh_b.mjs → media-usage-work-CLo_6tct.mjs} +2 -2
- package/dist/{media-usage-work-B9Bksh_b.mjs.map → media-usage-work-CLo_6tct.mjs.map} +1 -1
- package/dist/{menus-CC-yn1F9.mjs → menus-DSo5QeW0.mjs} +6 -6
- package/dist/{menus-CC-yn1F9.mjs.map → menus-DSo5QeW0.mjs.map} +1 -1
- package/dist/{menus-fxvEDYE6.mjs → menus-TCNSd2vp.mjs} +2 -2
- package/dist/{menus-fxvEDYE6.mjs.map → menus-TCNSd2vp.mjs.map} +1 -1
- package/dist/migrations/index.d.mts +22 -0
- package/dist/migrations/index.d.mts.map +1 -0
- package/dist/migrations/index.mjs +7 -0
- package/dist/{mode-Bpe-tNNy.mjs → mode-BDoH_WcF.mjs} +1 -1
- package/dist/{mode-Bpe-tNNy.mjs.map → mode-BDoH_WcF.mjs.map} +1 -1
- package/dist/{oauth-authorization-BXO1MKej.mjs → oauth-authorization-iOikmrJd.mjs} +4 -4
- package/dist/{oauth-authorization-BXO1MKej.mjs.map → oauth-authorization-iOikmrJd.mjs.map} +1 -1
- package/dist/{oauth-clients-C_DNqtRu.mjs → oauth-clients-CvI0nHMJ.mjs} +1 -1
- package/dist/{oauth-clients-C_DNqtRu.mjs.map → oauth-clients-CvI0nHMJ.mjs.map} +1 -1
- package/dist/{oauth-state-store-DwAxMtk4.mjs → oauth-state-store-Ba8KZlwW.mjs} +1 -1
- package/dist/{oauth-state-store-DwAxMtk4.mjs.map → oauth-state-store-Ba8KZlwW.mjs.map} +1 -1
- package/dist/{oauth-user-lookup-BxfZizdk.mjs → oauth-user-lookup-C5SQwGYC.mjs} +1 -1
- package/dist/{oauth-user-lookup-BxfZizdk.mjs.map → oauth-user-lookup-C5SQwGYC.mjs.map} +1 -1
- package/dist/object-cache/memory.d.mts +1 -1
- package/dist/{options-DRe1pIBL.d.mts → options--hhz-jZ1.d.mts} +3 -3
- package/dist/{options-DRe1pIBL.d.mts.map → options--hhz-jZ1.d.mts.map} +1 -1
- package/dist/page/index.d.mts +2 -2
- package/dist/{passkey-config-_2GOwwK8.mjs → passkey-config-DFAmUyuE.mjs} +1 -1
- package/dist/{passkey-config-_2GOwwK8.mjs.map → passkey-config-DFAmUyuE.mjs.map} +1 -1
- package/dist/{placeholder-CzW7Lshs.d.mts → placeholder-Dp0K3RkD.d.mts} +1 -1
- package/dist/{placeholder-CzW7Lshs.d.mts.map → placeholder-Dp0K3RkD.d.mts.map} +1 -1
- package/dist/plugin-types.d.mts +1 -1
- package/dist/plugin-utils.d.mts +10 -10
- package/dist/plugins/adapt-sandbox-entry.d.mts +10 -10
- package/dist/policy-TluUIxuu.mjs +45 -0
- package/dist/policy-TluUIxuu.mjs.map +1 -0
- package/dist/{portable-text-DhKbDAGr.mjs → portable-text-imBW5cHK.mjs} +1 -1
- package/dist/{portable-text-DhKbDAGr.mjs.map → portable-text-imBW5cHK.mjs.map} +1 -1
- package/dist/protocol-DjsJHTlK.d.mts +52 -0
- package/dist/protocol-DjsJHTlK.d.mts.map +1 -0
- package/dist/{query-vgZNwIhh.mjs → query-BWs7O5Il.mjs} +3 -3
- package/dist/{query-vgZNwIhh.mjs.map → query-BWs7O5Il.mjs.map} +1 -1
- package/dist/{query-BgjfiLUQ.mjs → query-zTVAR1Sl.mjs} +12 -12
- package/dist/{query-BgjfiLUQ.mjs.map → query-zTVAR1Sl.mjs.map} +1 -1
- package/dist/{rate-limit-L5anykgo.mjs → rate-limit-Bx4g9m3p.mjs} +1 -1
- package/dist/{rate-limit-L5anykgo.mjs.map → rate-limit-Bx4g9m3p.mjs.map} +1 -1
- package/dist/{redirect-KBNE3UEs.mjs → redirect-BgAOGHDq.mjs} +1 -1
- package/dist/{redirect-KBNE3UEs.mjs.map → redirect-BgAOGHDq.mjs.map} +1 -1
- package/dist/{redirects-cV9g7V-0.mjs → redirects-DTDHWH31.mjs} +2 -2
- package/dist/{redirects-cV9g7V-0.mjs.map → redirects-DTDHWH31.mjs.map} +1 -1
- package/dist/{redirects-CufRQt0Q.mjs → redirects-DyChwDkG.mjs} +1 -1
- package/dist/{redirects-CufRQt0Q.mjs.map → redirects-DyChwDkG.mjs.map} +1 -1
- package/dist/{registry-C581V4TP.mjs → registry-Cqa2I749.mjs} +6 -6
- package/dist/{registry-C581V4TP.mjs.map → registry-Cqa2I749.mjs.map} +1 -1
- package/dist/{relation-DtmAQH7s.mjs → relation-BnHB0T4J.mjs} +2 -2
- package/dist/{relation-DtmAQH7s.mjs.map → relation-BnHB0T4J.mjs.map} +1 -1
- package/dist/{relations-DItI3MN9.d.mts → relations-BHo5A8Zf.d.mts} +35 -35
- package/dist/{relations-DItI3MN9.d.mts.map → relations-BHo5A8Zf.d.mts.map} +1 -1
- package/dist/{relations-DICSyfsh.mjs → relations-D5sM5fW0.mjs} +4 -4
- package/dist/{relations-DICSyfsh.mjs.map → relations-D5sM5fW0.mjs.map} +1 -1
- package/dist/request-context.d.mts +1 -1
- package/dist/{runner-CgQbELYC.d.mts → runner-BYHZn3Jy.d.mts} +16 -3
- package/dist/runner-BYHZn3Jy.d.mts.map +1 -0
- package/dist/{runner-BiDvq5dv.mjs → runner-C1T11CfC.mjs} +34 -4
- package/dist/{runner-BiDvq5dv.mjs.map → runner-C1T11CfC.mjs.map} +1 -1
- package/dist/runtime.d.mts +11 -11
- package/dist/runtime.mjs +1 -1
- package/dist/{schema-CeQSNwB3.mjs → schema-2AOsSn2v.mjs} +4 -4
- package/dist/{schema-CeQSNwB3.mjs.map → schema-2AOsSn2v.mjs.map} +1 -1
- package/dist/{schema-BFyTg38T.mjs → schema-DZ5YVVZj.mjs} +3 -3
- package/dist/{schema-BFyTg38T.mjs.map → schema-DZ5YVVZj.mjs.map} +1 -1
- package/dist/{scopes-BVeuhGBE.mjs → scopes-8ClPF2bC.mjs} +2 -2
- package/dist/{scopes-BVeuhGBE.mjs.map → scopes-8ClPF2bC.mjs.map} +1 -1
- package/dist/{search-BpuPKZb0.mjs → search-u1QYdOCG.mjs} +4 -4
- package/dist/{search-BpuPKZb0.mjs.map → search-u1QYdOCG.mjs.map} +1 -1
- package/dist/{secrets-dtYs2hDo.mjs → secrets-BEgsAQEU.mjs} +1 -1
- package/dist/{secrets-dtYs2hDo.mjs.map → secrets-BEgsAQEU.mjs.map} +1 -1
- package/dist/{sections-COctk6hf.mjs → sections-DmHviGyM.mjs} +2 -2
- package/dist/{sections-COctk6hf.mjs.map → sections-DmHviGyM.mjs.map} +1 -1
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +13 -13
- package/dist/seo/index.d.mts +3 -3
- package/dist/seo/index.mjs +9 -9
- package/dist/{seo-CvDxfvFb.mjs → seo-Dq4G-RrO.mjs} +2 -2
- package/dist/{seo-CvDxfvFb.mjs.map → seo-Dq4G-RrO.mjs.map} +1 -1
- package/dist/{service-xMiMSTsR.mjs → service-BpArayPr.mjs} +2 -2
- package/dist/{service-xMiMSTsR.mjs.map → service-BpArayPr.mjs.map} +1 -1
- package/dist/{session-user-BzVemLkw.mjs → session-user-BEorKVT-.mjs} +1 -1
- package/dist/{session-user-BzVemLkw.mjs.map → session-user-BEorKVT-.mjs.map} +1 -1
- package/dist/{settings-BE9aqn_9.mjs → settings-DE9xtzqd.mjs} +3 -3
- package/dist/{settings-BE9aqn_9.mjs.map → settings-DE9xtzqd.mjs.map} +1 -1
- package/dist/{settings-B372MV0p.mjs → settings-xRdoUZr0.mjs} +2 -2
- package/dist/{settings-B372MV0p.mjs.map → settings-xRdoUZr0.mjs.map} +1 -1
- package/dist/{setup-complete-DF7TAfD5.mjs → setup-complete-BnT8pq_C.mjs} +1 -1
- package/dist/{setup-complete-DF7TAfD5.mjs.map → setup-complete-BnT8pq_C.mjs.map} +1 -1
- package/dist/{setup-nonce-Do-U_cg9.mjs → setup-nonce-CBitf_oQ.mjs} +1 -1
- package/dist/{setup-nonce-Do-U_cg9.mjs.map → setup-nonce-CBitf_oQ.mjs.map} +1 -1
- package/dist/{site-url-Bowsi9T-.mjs → site-url-DjpIXMXJ.mjs} +1 -1
- package/dist/{site-url-Bowsi9T-.mjs.map → site-url-DjpIXMXJ.mjs.map} +1 -1
- package/dist/{snapshot-B5h6aPMh.mjs → snapshot-BBlGkb2q.mjs} +1 -1
- package/dist/{snapshot-B5h6aPMh.mjs.map → snapshot-BBlGkb2q.mjs.map} +1 -1
- package/dist/storage/local.d.mts +1 -1
- package/dist/storage/local.mjs +2 -2
- package/dist/storage/s3.d.mts +1 -1
- package/dist/target-DixqqV5i.mjs +27 -0
- package/dist/target-DixqqV5i.mjs.map +1 -0
- package/dist/{taxonomies-B0o9ExNV.mjs → taxonomies-BlrncHi9.mjs} +5 -5
- package/dist/{taxonomies-B0o9ExNV.mjs.map → taxonomies-BlrncHi9.mjs.map} +1 -1
- package/dist/{taxonomies-Dk3MAklL.mjs → taxonomies-OHUIiIpI.mjs} +4 -4
- package/dist/{taxonomies-Dk3MAklL.mjs.map → taxonomies-OHUIiIpI.mjs.map} +1 -1
- package/dist/{taxonomy-CPeM8d2o.mjs → taxonomy-BqCZ1TdU.mjs} +3 -3
- package/dist/{taxonomy-CPeM8d2o.mjs.map → taxonomy-BqCZ1TdU.mjs.map} +1 -1
- package/dist/{transport--CfOflrp.mjs → transport-DBJwa-Si.mjs} +1 -1
- package/dist/{transport--CfOflrp.mjs.map → transport-DBJwa-Si.mjs.map} +1 -1
- package/dist/{transport-B_rfeU-a.d.mts → transport-RQE36APG.d.mts} +1 -1
- package/dist/{transport-B_rfeU-a.d.mts.map → transport-RQE36APG.d.mts.map} +1 -1
- package/dist/{types-CFUfyt1s.d.mts → types-BcqdcT-r.d.mts} +1 -1
- package/dist/{types-CFUfyt1s.d.mts.map → types-BcqdcT-r.d.mts.map} +1 -1
- package/dist/{types-CPx2lIGo.d.mts → types-BntycPTF.d.mts} +1 -1
- package/dist/{types-CPx2lIGo.d.mts.map → types-BntycPTF.d.mts.map} +1 -1
- package/dist/{types-C3_AbkDP.d.mts → types-C2pMvO62.d.mts} +1 -1
- package/dist/{types-C3_AbkDP.d.mts.map → types-C2pMvO62.d.mts.map} +1 -1
- package/dist/{types-DefV3OiO.d.mts → types-CXnOzDe1.d.mts} +1 -1
- package/dist/{types-DefV3OiO.d.mts.map → types-CXnOzDe1.d.mts.map} +1 -1
- package/dist/{types-D4_1Ga4L.d.mts → types-DN64_zzZ.d.mts} +3 -3
- package/dist/{types-D4_1Ga4L.d.mts.map → types-DN64_zzZ.d.mts.map} +1 -1
- package/dist/{types-CAGdePf3.d.mts → types-DQIDRKF2.d.mts} +1 -1
- package/dist/{types-CAGdePf3.d.mts.map → types-DQIDRKF2.d.mts.map} +1 -1
- package/dist/{types-BPuloofb.d.mts → types-DWRhk9xZ.d.mts} +1 -1
- package/dist/{types-BPuloofb.d.mts.map → types-DWRhk9xZ.d.mts.map} +1 -1
- package/dist/{types-X0MThr7x.d.mts → types-Dfk8KI5R.d.mts} +1 -1
- package/dist/{types-X0MThr7x.d.mts.map → types-Dfk8KI5R.d.mts.map} +1 -1
- package/dist/{types-Cs-TFLNf.d.mts → types-DrJrVCT1.d.mts} +3 -3
- package/dist/{types-Cs-TFLNf.d.mts.map → types-DrJrVCT1.d.mts.map} +1 -1
- package/dist/{upload-attempts-hlne3iA1.mjs → upload-attempts-D4FS8LT6.mjs} +1 -1
- package/dist/{upload-attempts-hlne3iA1.mjs.map → upload-attempts-D4FS8LT6.mjs.map} +1 -1
- package/dist/{user-sHgJ62SP.mjs → user-DJZfUJVE.mjs} +2 -2
- package/dist/{user-sHgJ62SP.mjs.map → user-DJZfUJVE.mjs.map} +1 -1
- package/dist/{validate-BkqIG_Uj.mjs → validate-BMLBvxOC.mjs} +2 -2
- package/dist/{validate-BkqIG_Uj.mjs.map → validate-BMLBvxOC.mjs.map} +1 -1
- package/dist/{validate-DeGtBN84.d.mts → validate-CTqt5nuE.d.mts} +5 -5
- package/dist/{validate-DeGtBN84.d.mts.map → validate-CTqt5nuE.d.mts.map} +1 -1
- package/dist/{validation-CiJb_X5o.mjs → validation-npvENUCB.mjs} +7 -7
- package/dist/{validation-CiJb_X5o.mjs.map → validation-npvENUCB.mjs.map} +1 -1
- package/dist/version-DB_fqCSI.mjs +7 -0
- package/dist/{version-x0NjTScG.mjs.map → version-DB_fqCSI.mjs.map} +1 -1
- package/dist/{widgets-Cwzvww0W.mjs → widgets-BD3ho7Rq.mjs} +2 -2
- package/dist/{widgets-Cwzvww0W.mjs.map → widgets-BD3ho7Rq.mjs.map} +1 -1
- package/dist/{wxr-taxonomies-QZjWxjyk.mjs → wxr-taxonomies-B1R3vBoD.mjs} +3 -3
- package/dist/{wxr-taxonomies-QZjWxjyk.mjs.map → wxr-taxonomies-B1R3vBoD.mjs.map} +1 -1
- package/dist/{wxr-taxonomies-CmC0ZsDr.d.mts → wxr-taxonomies-Ca68766D.d.mts} +1 -1
- package/dist/{wxr-taxonomies-CmC0ZsDr.d.mts.map → wxr-taxonomies-Ca68766D.d.mts.map} +1 -1
- package/dist/{zod-generator-D11MhxG4.mjs → zod-generator-LC-on_Vb.mjs} +2 -2
- package/dist/{zod-generator-D11MhxG4.mjs.map → zod-generator-LC-on_Vb.mjs.map} +1 -1
- package/package.json +21 -5
- package/src/astro/integration/index.ts +46 -14
- package/src/astro/integration/runtime.ts +3 -0
- package/src/astro/middleware/scoped-db.ts +21 -0
- package/src/astro/middleware.ts +67 -10
- package/src/astro/routes/api/auth/dev-bypass.ts +0 -4
- package/src/astro/routes/api/setup/dev-bypass.ts +2 -7
- package/src/astro/routes/api/setup/index.ts +5 -13
- package/src/cli/commands/migrate.ts +593 -0
- package/src/cli/index.ts +2 -23
- package/src/database/index.ts +8 -2
- package/src/database/migrations/policy.ts +70 -0
- package/src/database/migrations/runner.ts +49 -3
- package/src/db/adapters.ts +50 -2
- package/src/db/index.ts +3 -1
- package/src/db/libsql-migrations.ts +72 -0
- package/src/db/postgres-migrations.ts +59 -0
- package/src/db/sqlite-migrations.ts +37 -0
- package/src/emdash-runtime.ts +44 -15
- package/src/i18n/normalize.ts +31 -0
- package/src/index.ts +7 -1
- package/src/migrations/config-loader.ts +205 -0
- package/src/migrations/direct-executor.ts +141 -0
- package/src/migrations/identity.ts +46 -0
- package/src/migrations/index.ts +19 -0
- package/src/migrations/integration-metadata.ts +67 -0
- package/src/migrations/manifest-builder.ts +55 -0
- package/src/migrations/manifest-writer.ts +63 -0
- package/src/migrations/manifest.ts +197 -0
- package/src/migrations/protocol.ts +59 -0
- package/src/migrations/target.ts +35 -0
- package/src/virtual-modules.d.ts +8 -0
- package/dist/adapters-34EN2eXO.d.mts.map +0 -1
- package/dist/index-C3Y-rsyN.d.mts.map +0 -1
- package/dist/runner-CgQbELYC.d.mts.map +0 -1
- package/dist/version-x0NjTScG.mjs +0 -7
- /package/dist/{api-tokens-CyTsLszZ.mjs → api-tokens-rIXC6LdE.mjs} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sections-COctk6hf.mjs","names":[],"sources":["../src/sections/index.ts","../src/api/handlers/sections.ts"],"sourcesContent":["/**\n * Sections runtime functions\n *\n * Sections are reusable content blocks that can be inserted into any Portable Text field.\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { encodeCursor, decodeCursor, type FindManyResult } from \"../database/repositories/types.js\";\nimport type { Database } from \"../database/types.js\";\nimport { getDb } from \"../loader.js\";\nimport type { Section, SectionRow, GetSectionsOptions } from \"./types.js\";\n\nexport type {\n\tSection,\n\tSectionSource,\n\tSectionRow,\n\tCreateSectionInput,\n\tUpdateSectionInput,\n\tGetSectionsOptions,\n} from \"./types.js\";\n\n/**\n * Get a section by slug\n *\n * @example\n * ```ts\n * import { getSection } from \"emdash\";\n *\n * const section = await getSection(\"hero-centered\");\n * if (section) {\n * console.log(section.content); // Portable Text array\n * }\n * ```\n */\nexport async function getSection(slug: string): Promise<Section | null> {\n\tconst db = await getDb();\n\treturn getSectionWithDb(slug, db);\n}\n\n/**\n * Get a section by slug (with explicit db)\n *\n * @internal Use `getSection()` in templates. This variant is for admin routes\n * that already have a database handle.\n */\nexport async function getSectionWithDb(\n\tslug: string,\n\tdb: Kysely<Database>,\n): Promise<Section | null> {\n\tconst row = await db\n\t\t.selectFrom(\"_emdash_sections\")\n\t\t.selectAll()\n\t\t.$castTo<SectionRow>()\n\t\t.where(\"slug\", \"=\", slug)\n\t\t.executeTakeFirst();\n\n\tif (!row) {\n\t\treturn null;\n\t}\n\n\treturn rowToSection(row, db);\n}\n\n/**\n * Get a section by ID\n *\n * @internal Primarily for admin use\n */\nexport async function getSectionById(id: string, db: Kysely<Database>): Promise<Section | null> {\n\tconst row = await db\n\t\t.selectFrom(\"_emdash_sections\")\n\t\t.selectAll()\n\t\t.$castTo<SectionRow>()\n\t\t.where(\"id\", \"=\", id)\n\t\t.executeTakeFirst();\n\n\tif (!row) {\n\t\treturn null;\n\t}\n\n\treturn rowToSection(row, db);\n}\n\n/**\n * Get all sections with optional filtering\n *\n * @example\n * ```ts\n * import { getSections } from \"emdash\";\n *\n * // Get all theme-provided sections\n * const themeSections = await getSections({ source: \"theme\" });\n *\n * // Search sections\n * const results = await getSections({ search: \"pricing\" });\n * ```\n */\nexport async function getSections(\n\toptions: GetSectionsOptions = {},\n): Promise<FindManyResult<Section>> {\n\tconst db = await getDb();\n\treturn getSectionsWithDb(db, options);\n}\n\n/**\n * Get all sections with optional filtering (with explicit db)\n *\n * @internal Use `getSections()` in templates. This variant is for admin routes\n * that already have a database handle.\n */\nexport async function getSectionsWithDb(\n\tdb: Kysely<Database>,\n\toptions: GetSectionsOptions = {},\n): Promise<FindManyResult<Section>> {\n\tconst limit = Math.min(Math.max(1, options.limit || 50), 100);\n\n\tlet query = db.selectFrom(\"_emdash_sections\").selectAll();\n\n\t// Filter by source\n\tif (options.source) {\n\t\tquery = query.where(\"source\", \"=\", options.source);\n\t}\n\n\t// Search - search title, description, and keywords\n\tif (options.search) {\n\t\tconst searchTerm = `%${options.search.toLowerCase()}%`;\n\t\tquery = query.where((eb) =>\n\t\t\teb.or([\n\t\t\t\teb(\"title\", \"like\", searchTerm),\n\t\t\t\teb(\"description\", \"like\", searchTerm),\n\t\t\t\teb(\"keywords\", \"like\", searchTerm),\n\t\t\t]),\n\t\t);\n\t}\n\n\t// Order by title ASC, id ASC for stable cursor pagination\n\tquery = query.orderBy(\"title\", \"asc\").orderBy(\"id\", \"asc\");\n\n\t// Cursor-based pagination — throws on invalid cursor.\n\tif (options.cursor) {\n\t\tconst decoded = decodeCursor(options.cursor);\n\t\tquery = query.where((eb) =>\n\t\t\teb.or([\n\t\t\t\teb(\"title\", \">\", decoded.orderValue),\n\t\t\t\teb.and([eb(\"title\", \"=\", decoded.orderValue), eb(\"id\", \">\", decoded.id)]),\n\t\t\t]),\n\t\t);\n\t}\n\n\tquery = query.limit(limit + 1);\n\n\tconst rows = await query.$castTo<SectionRow>().execute();\n\tconst hasMore = rows.length > limit;\n\tconst sliced = rows.slice(0, limit);\n\n\t// Convert rows to sections\n\tconst items = await Promise.all(sliced.map((row) => rowToSection(row, db)));\n\tconst result: FindManyResult<Section> = { items };\n\n\tif (hasMore && items.length > 0) {\n\t\tconst last = items.at(-1)!;\n\t\tresult.nextCursor = encodeCursor(last.title, last.id);\n\t}\n\n\treturn result;\n}\n\n/**\n * Convert a section row to the API type\n */\nasync function rowToSection(row: SectionRow, db: Kysely<Database>): Promise<Section> {\n\t// Parse keywords\n\tlet keywords: string[] = [];\n\tif (row.keywords) {\n\t\ttry {\n\t\t\tkeywords = JSON.parse(row.keywords);\n\t\t} catch {\n\t\t\t// Invalid JSON, ignore\n\t\t}\n\t}\n\n\t// Parse content — stored as JSON array of Portable Text blocks\n\tlet content: Section[\"content\"] = [];\n\tif (row.content) {\n\t\ttry {\n\t\t\tconst parsed: unknown = JSON.parse(row.content);\n\t\t\tif (Array.isArray(parsed)) {\n\t\t\t\t// DB stores serialized PortableTextBlock[]; trust the schema\n\t\t\t\tcontent = parsed;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Invalid JSON, ignore\n\t\t}\n\t}\n\n\t// Get preview URL from media (if present)\n\tlet previewUrl: string | undefined;\n\tif (row.preview_media_id) {\n\t\tconst media = await db\n\t\t\t.selectFrom(\"media\")\n\t\t\t.select(\"storage_key\")\n\t\t\t.where(\"id\", \"=\", row.preview_media_id)\n\t\t\t.executeTakeFirst();\n\n\t\tif (media) {\n\t\t\tpreviewUrl = `/_emdash/media/${media.storage_key}`;\n\t\t}\n\t}\n\n\treturn {\n\t\tid: row.id,\n\t\tslug: row.slug,\n\t\ttitle: row.title,\n\t\tdescription: row.description ?? undefined,\n\t\tkeywords,\n\t\tcontent,\n\t\tpreviewUrl,\n\t\tsource: row.source,\n\t\tthemeId: row.theme_id ?? undefined,\n\t\tcreatedAt: row.created_at,\n\t\tupdatedAt: row.updated_at,\n\t};\n}\n","/**\n * Section CRUD handlers\n */\n\nimport type { Kysely } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport { InvalidCursorError } from \"../../database/repositories/types.js\";\nimport type { FindManyResult } from \"../../database/repositories/types.js\";\nimport type { Database } from \"../../database/types.js\";\nimport {\n\tgetSectionById,\n\tgetSectionWithDb,\n\tgetSectionsWithDb,\n\ttype Section,\n\ttype GetSectionsOptions,\n} from \"../../sections/index.js\";\nimport type { ApiResult } from \"../types.js\";\n\nconst SLUG_PATTERN = /^[a-z0-9-]+$/;\n\nexport type SectionListResponse = FindManyResult<Section>;\n\n/**\n * List sections with optional filters\n */\nexport async function handleSectionList(\n\tdb: Kysely<Database>,\n\tparams: GetSectionsOptions,\n): Promise<ApiResult<SectionListResponse>> {\n\ttry {\n\t\tconst result = await getSectionsWithDb(db, {\n\t\t\tsource: params.source,\n\t\t\tsearch: params.search,\n\t\t\tlimit: params.limit,\n\t\t\tcursor: params.cursor,\n\t\t});\n\n\t\treturn { success: true, data: result };\n\t} catch (error) {\n\t\tif (error instanceof InvalidCursorError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"INVALID_CURSOR\", message: error.message },\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"SECTION_LIST_ERROR\", message: \"Failed to fetch sections\" },\n\t\t};\n\t}\n}\n\n/**\n * Create a section\n */\nexport async function handleSectionCreate(\n\tdb: Kysely<Database>,\n\tinput: {\n\t\tslug: string;\n\t\ttitle: string;\n\t\tdescription?: string;\n\t\tkeywords?: string[];\n\t\tcontent: unknown[];\n\t\tpreviewMediaId?: string;\n\t\tsource?: string;\n\t\tthemeId?: string;\n\t},\n): Promise<ApiResult<Section>> {\n\ttry {\n\t\t// Validate slug format\n\t\tif (!SLUG_PATTERN.test(input.slug)) {\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: \"slug must only contain lowercase letters, numbers, and hyphens\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Check if slug already exists\n\t\tconst existing = await db\n\t\t\t.selectFrom(\"_emdash_sections\")\n\t\t\t.select(\"id\")\n\t\t\t.where(\"slug\", \"=\", input.slug)\n\t\t\t.executeTakeFirst();\n\n\t\tif (existing) {\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: `Section with slug \"${input.slug}\" already exists`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst id = ulid();\n\t\tconst now = new Date().toISOString();\n\n\t\tawait db\n\t\t\t.insertInto(\"_emdash_sections\")\n\t\t\t.values({\n\t\t\t\tid,\n\t\t\t\tslug: input.slug,\n\t\t\t\ttitle: input.title,\n\t\t\t\tdescription: input.description ?? null,\n\t\t\t\tkeywords: input.keywords ? JSON.stringify(input.keywords) : null,\n\t\t\t\tcontent: JSON.stringify(input.content),\n\t\t\t\tpreview_media_id: input.previewMediaId ?? null,\n\t\t\t\tsource: input.source ?? \"user\",\n\t\t\t\ttheme_id: input.themeId ?? null,\n\t\t\t\tcreated_at: now,\n\t\t\t\tupdated_at: now,\n\t\t\t})\n\t\t\t.execute();\n\n\t\tconst section = await getSectionById(id, db);\n\t\tif (!section) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"SECTION_CREATE_ERROR\", message: \"Failed to fetch created section\" },\n\t\t\t};\n\t\t}\n\n\t\treturn { success: true, data: section };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"SECTION_CREATE_ERROR\", message: \"Failed to create section\" },\n\t\t};\n\t}\n}\n\n/**\n * Get a section by slug\n */\nexport async function handleSectionGet(\n\tdb: Kysely<Database>,\n\tslug: string,\n): Promise<ApiResult<Section>> {\n\ttry {\n\t\tconst section = await getSectionWithDb(slug, db);\n\n\t\tif (!section) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: `Section \"${slug}\" not found` },\n\t\t\t};\n\t\t}\n\n\t\treturn { success: true, data: section };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"SECTION_GET_ERROR\", message: \"Failed to fetch section\" },\n\t\t};\n\t}\n}\n\n/**\n * Update a section by slug\n */\nexport async function handleSectionUpdate(\n\tdb: Kysely<Database>,\n\tslug: string,\n\tinput: {\n\t\tslug?: string;\n\t\ttitle?: string;\n\t\tdescription?: string;\n\t\tkeywords?: string[];\n\t\tcontent?: unknown[];\n\t\tpreviewMediaId?: string | null;\n\t},\n): Promise<ApiResult<Section>> {\n\ttry {\n\t\t// Check if section exists\n\t\tconst existing = await db\n\t\t\t.selectFrom(\"_emdash_sections\")\n\t\t\t.select([\"id\", \"source\"])\n\t\t\t.where(\"slug\", \"=\", slug)\n\t\t\t.executeTakeFirst();\n\n\t\tif (!existing) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: `Section \"${slug}\" not found` },\n\t\t\t};\n\t\t}\n\n\t\t// Validate new slug if changing\n\t\tif (input.slug && input.slug !== slug) {\n\t\t\tif (!SLUG_PATTERN.test(input.slug)) {\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: \"slug must only contain lowercase letters, numbers, and hyphens\",\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Check if new slug already exists\n\t\t\tconst slugExists = await db\n\t\t\t\t.selectFrom(\"_emdash_sections\")\n\t\t\t\t.select(\"id\")\n\t\t\t\t.where(\"slug\", \"=\", input.slug)\n\t\t\t\t.executeTakeFirst();\n\n\t\t\tif (slugExists) {\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: \"CONFLICT\",\n\t\t\t\t\t\tmessage: `Section with slug \"${input.slug}\" already exists`,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\t// Build update object\n\t\tconst updates: Record<string, unknown> = {\n\t\t\tupdated_at: new Date().toISOString(),\n\t\t};\n\n\t\tif (input.slug !== undefined) updates.slug = input.slug;\n\t\tif (input.title !== undefined) updates.title = input.title;\n\t\tif (input.description !== undefined) updates.description = input.description;\n\t\tif (input.keywords !== undefined) updates.keywords = JSON.stringify(input.keywords);\n\t\tif (input.content !== undefined) updates.content = JSON.stringify(input.content);\n\t\tif (input.previewMediaId !== undefined) updates.preview_media_id = input.previewMediaId;\n\n\t\tawait db.updateTable(\"_emdash_sections\").set(updates).where(\"id\", \"=\", existing.id).execute();\n\n\t\tconst section = await getSectionById(existing.id, db);\n\t\tif (!section) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"SECTION_UPDATE_ERROR\", message: \"Failed to fetch updated section\" },\n\t\t\t};\n\t\t}\n\n\t\treturn { success: true, data: section };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"SECTION_UPDATE_ERROR\", message: \"Failed to update section\" },\n\t\t};\n\t}\n}\n\n/**\n * Delete a section by slug\n */\nexport async function handleSectionDelete(\n\tdb: Kysely<Database>,\n\tslug: string,\n): Promise<ApiResult<{ deleted: true }>> {\n\ttry {\n\t\t// Check if section exists and get source\n\t\tconst existing = await db\n\t\t\t.selectFrom(\"_emdash_sections\")\n\t\t\t.select([\"id\", \"source\", \"theme_id\"])\n\t\t\t.where(\"slug\", \"=\", slug)\n\t\t\t.executeTakeFirst();\n\n\t\tif (!existing) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: `Section \"${slug}\" not found` },\n\t\t\t};\n\t\t}\n\n\t\t// Prevent deleting theme sections\n\t\tif (existing.source === \"theme\") {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"FORBIDDEN\",\n\t\t\t\t\tmessage:\n\t\t\t\t\t\t\"Cannot delete theme-provided sections. Edit the section to create a user copy, then delete that.\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tawait db.deleteFrom(\"_emdash_sections\").where(\"id\", \"=\", existing.id).execute();\n\n\t\treturn { success: true, data: { deleted: true } };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"SECTION_DELETE_ERROR\", message: \"Failed to delete section\" },\n\t\t};\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sections-DmHviGyM.mjs","names":[],"sources":["../src/sections/index.ts","../src/api/handlers/sections.ts"],"sourcesContent":["/**\n * Sections runtime functions\n *\n * Sections are reusable content blocks that can be inserted into any Portable Text field.\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { encodeCursor, decodeCursor, type FindManyResult } from \"../database/repositories/types.js\";\nimport type { Database } from \"../database/types.js\";\nimport { getDb } from \"../loader.js\";\nimport type { Section, SectionRow, GetSectionsOptions } from \"./types.js\";\n\nexport type {\n\tSection,\n\tSectionSource,\n\tSectionRow,\n\tCreateSectionInput,\n\tUpdateSectionInput,\n\tGetSectionsOptions,\n} from \"./types.js\";\n\n/**\n * Get a section by slug\n *\n * @example\n * ```ts\n * import { getSection } from \"emdash\";\n *\n * const section = await getSection(\"hero-centered\");\n * if (section) {\n * console.log(section.content); // Portable Text array\n * }\n * ```\n */\nexport async function getSection(slug: string): Promise<Section | null> {\n\tconst db = await getDb();\n\treturn getSectionWithDb(slug, db);\n}\n\n/**\n * Get a section by slug (with explicit db)\n *\n * @internal Use `getSection()` in templates. This variant is for admin routes\n * that already have a database handle.\n */\nexport async function getSectionWithDb(\n\tslug: string,\n\tdb: Kysely<Database>,\n): Promise<Section | null> {\n\tconst row = await db\n\t\t.selectFrom(\"_emdash_sections\")\n\t\t.selectAll()\n\t\t.$castTo<SectionRow>()\n\t\t.where(\"slug\", \"=\", slug)\n\t\t.executeTakeFirst();\n\n\tif (!row) {\n\t\treturn null;\n\t}\n\n\treturn rowToSection(row, db);\n}\n\n/**\n * Get a section by ID\n *\n * @internal Primarily for admin use\n */\nexport async function getSectionById(id: string, db: Kysely<Database>): Promise<Section | null> {\n\tconst row = await db\n\t\t.selectFrom(\"_emdash_sections\")\n\t\t.selectAll()\n\t\t.$castTo<SectionRow>()\n\t\t.where(\"id\", \"=\", id)\n\t\t.executeTakeFirst();\n\n\tif (!row) {\n\t\treturn null;\n\t}\n\n\treturn rowToSection(row, db);\n}\n\n/**\n * Get all sections with optional filtering\n *\n * @example\n * ```ts\n * import { getSections } from \"emdash\";\n *\n * // Get all theme-provided sections\n * const themeSections = await getSections({ source: \"theme\" });\n *\n * // Search sections\n * const results = await getSections({ search: \"pricing\" });\n * ```\n */\nexport async function getSections(\n\toptions: GetSectionsOptions = {},\n): Promise<FindManyResult<Section>> {\n\tconst db = await getDb();\n\treturn getSectionsWithDb(db, options);\n}\n\n/**\n * Get all sections with optional filtering (with explicit db)\n *\n * @internal Use `getSections()` in templates. This variant is for admin routes\n * that already have a database handle.\n */\nexport async function getSectionsWithDb(\n\tdb: Kysely<Database>,\n\toptions: GetSectionsOptions = {},\n): Promise<FindManyResult<Section>> {\n\tconst limit = Math.min(Math.max(1, options.limit || 50), 100);\n\n\tlet query = db.selectFrom(\"_emdash_sections\").selectAll();\n\n\t// Filter by source\n\tif (options.source) {\n\t\tquery = query.where(\"source\", \"=\", options.source);\n\t}\n\n\t// Search - search title, description, and keywords\n\tif (options.search) {\n\t\tconst searchTerm = `%${options.search.toLowerCase()}%`;\n\t\tquery = query.where((eb) =>\n\t\t\teb.or([\n\t\t\t\teb(\"title\", \"like\", searchTerm),\n\t\t\t\teb(\"description\", \"like\", searchTerm),\n\t\t\t\teb(\"keywords\", \"like\", searchTerm),\n\t\t\t]),\n\t\t);\n\t}\n\n\t// Order by title ASC, id ASC for stable cursor pagination\n\tquery = query.orderBy(\"title\", \"asc\").orderBy(\"id\", \"asc\");\n\n\t// Cursor-based pagination — throws on invalid cursor.\n\tif (options.cursor) {\n\t\tconst decoded = decodeCursor(options.cursor);\n\t\tquery = query.where((eb) =>\n\t\t\teb.or([\n\t\t\t\teb(\"title\", \">\", decoded.orderValue),\n\t\t\t\teb.and([eb(\"title\", \"=\", decoded.orderValue), eb(\"id\", \">\", decoded.id)]),\n\t\t\t]),\n\t\t);\n\t}\n\n\tquery = query.limit(limit + 1);\n\n\tconst rows = await query.$castTo<SectionRow>().execute();\n\tconst hasMore = rows.length > limit;\n\tconst sliced = rows.slice(0, limit);\n\n\t// Convert rows to sections\n\tconst items = await Promise.all(sliced.map((row) => rowToSection(row, db)));\n\tconst result: FindManyResult<Section> = { items };\n\n\tif (hasMore && items.length > 0) {\n\t\tconst last = items.at(-1)!;\n\t\tresult.nextCursor = encodeCursor(last.title, last.id);\n\t}\n\n\treturn result;\n}\n\n/**\n * Convert a section row to the API type\n */\nasync function rowToSection(row: SectionRow, db: Kysely<Database>): Promise<Section> {\n\t// Parse keywords\n\tlet keywords: string[] = [];\n\tif (row.keywords) {\n\t\ttry {\n\t\t\tkeywords = JSON.parse(row.keywords);\n\t\t} catch {\n\t\t\t// Invalid JSON, ignore\n\t\t}\n\t}\n\n\t// Parse content — stored as JSON array of Portable Text blocks\n\tlet content: Section[\"content\"] = [];\n\tif (row.content) {\n\t\ttry {\n\t\t\tconst parsed: unknown = JSON.parse(row.content);\n\t\t\tif (Array.isArray(parsed)) {\n\t\t\t\t// DB stores serialized PortableTextBlock[]; trust the schema\n\t\t\t\tcontent = parsed;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Invalid JSON, ignore\n\t\t}\n\t}\n\n\t// Get preview URL from media (if present)\n\tlet previewUrl: string | undefined;\n\tif (row.preview_media_id) {\n\t\tconst media = await db\n\t\t\t.selectFrom(\"media\")\n\t\t\t.select(\"storage_key\")\n\t\t\t.where(\"id\", \"=\", row.preview_media_id)\n\t\t\t.executeTakeFirst();\n\n\t\tif (media) {\n\t\t\tpreviewUrl = `/_emdash/media/${media.storage_key}`;\n\t\t}\n\t}\n\n\treturn {\n\t\tid: row.id,\n\t\tslug: row.slug,\n\t\ttitle: row.title,\n\t\tdescription: row.description ?? undefined,\n\t\tkeywords,\n\t\tcontent,\n\t\tpreviewUrl,\n\t\tsource: row.source,\n\t\tthemeId: row.theme_id ?? undefined,\n\t\tcreatedAt: row.created_at,\n\t\tupdatedAt: row.updated_at,\n\t};\n}\n","/**\n * Section CRUD handlers\n */\n\nimport type { Kysely } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport { InvalidCursorError } from \"../../database/repositories/types.js\";\nimport type { FindManyResult } from \"../../database/repositories/types.js\";\nimport type { Database } from \"../../database/types.js\";\nimport {\n\tgetSectionById,\n\tgetSectionWithDb,\n\tgetSectionsWithDb,\n\ttype Section,\n\ttype GetSectionsOptions,\n} from \"../../sections/index.js\";\nimport type { ApiResult } from \"../types.js\";\n\nconst SLUG_PATTERN = /^[a-z0-9-]+$/;\n\nexport type SectionListResponse = FindManyResult<Section>;\n\n/**\n * List sections with optional filters\n */\nexport async function handleSectionList(\n\tdb: Kysely<Database>,\n\tparams: GetSectionsOptions,\n): Promise<ApiResult<SectionListResponse>> {\n\ttry {\n\t\tconst result = await getSectionsWithDb(db, {\n\t\t\tsource: params.source,\n\t\t\tsearch: params.search,\n\t\t\tlimit: params.limit,\n\t\t\tcursor: params.cursor,\n\t\t});\n\n\t\treturn { success: true, data: result };\n\t} catch (error) {\n\t\tif (error instanceof InvalidCursorError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"INVALID_CURSOR\", message: error.message },\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"SECTION_LIST_ERROR\", message: \"Failed to fetch sections\" },\n\t\t};\n\t}\n}\n\n/**\n * Create a section\n */\nexport async function handleSectionCreate(\n\tdb: Kysely<Database>,\n\tinput: {\n\t\tslug: string;\n\t\ttitle: string;\n\t\tdescription?: string;\n\t\tkeywords?: string[];\n\t\tcontent: unknown[];\n\t\tpreviewMediaId?: string;\n\t\tsource?: string;\n\t\tthemeId?: string;\n\t},\n): Promise<ApiResult<Section>> {\n\ttry {\n\t\t// Validate slug format\n\t\tif (!SLUG_PATTERN.test(input.slug)) {\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: \"slug must only contain lowercase letters, numbers, and hyphens\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Check if slug already exists\n\t\tconst existing = await db\n\t\t\t.selectFrom(\"_emdash_sections\")\n\t\t\t.select(\"id\")\n\t\t\t.where(\"slug\", \"=\", input.slug)\n\t\t\t.executeTakeFirst();\n\n\t\tif (existing) {\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: `Section with slug \"${input.slug}\" already exists`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst id = ulid();\n\t\tconst now = new Date().toISOString();\n\n\t\tawait db\n\t\t\t.insertInto(\"_emdash_sections\")\n\t\t\t.values({\n\t\t\t\tid,\n\t\t\t\tslug: input.slug,\n\t\t\t\ttitle: input.title,\n\t\t\t\tdescription: input.description ?? null,\n\t\t\t\tkeywords: input.keywords ? JSON.stringify(input.keywords) : null,\n\t\t\t\tcontent: JSON.stringify(input.content),\n\t\t\t\tpreview_media_id: input.previewMediaId ?? null,\n\t\t\t\tsource: input.source ?? \"user\",\n\t\t\t\ttheme_id: input.themeId ?? null,\n\t\t\t\tcreated_at: now,\n\t\t\t\tupdated_at: now,\n\t\t\t})\n\t\t\t.execute();\n\n\t\tconst section = await getSectionById(id, db);\n\t\tif (!section) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"SECTION_CREATE_ERROR\", message: \"Failed to fetch created section\" },\n\t\t\t};\n\t\t}\n\n\t\treturn { success: true, data: section };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"SECTION_CREATE_ERROR\", message: \"Failed to create section\" },\n\t\t};\n\t}\n}\n\n/**\n * Get a section by slug\n */\nexport async function handleSectionGet(\n\tdb: Kysely<Database>,\n\tslug: string,\n): Promise<ApiResult<Section>> {\n\ttry {\n\t\tconst section = await getSectionWithDb(slug, db);\n\n\t\tif (!section) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: `Section \"${slug}\" not found` },\n\t\t\t};\n\t\t}\n\n\t\treturn { success: true, data: section };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"SECTION_GET_ERROR\", message: \"Failed to fetch section\" },\n\t\t};\n\t}\n}\n\n/**\n * Update a section by slug\n */\nexport async function handleSectionUpdate(\n\tdb: Kysely<Database>,\n\tslug: string,\n\tinput: {\n\t\tslug?: string;\n\t\ttitle?: string;\n\t\tdescription?: string;\n\t\tkeywords?: string[];\n\t\tcontent?: unknown[];\n\t\tpreviewMediaId?: string | null;\n\t},\n): Promise<ApiResult<Section>> {\n\ttry {\n\t\t// Check if section exists\n\t\tconst existing = await db\n\t\t\t.selectFrom(\"_emdash_sections\")\n\t\t\t.select([\"id\", \"source\"])\n\t\t\t.where(\"slug\", \"=\", slug)\n\t\t\t.executeTakeFirst();\n\n\t\tif (!existing) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: `Section \"${slug}\" not found` },\n\t\t\t};\n\t\t}\n\n\t\t// Validate new slug if changing\n\t\tif (input.slug && input.slug !== slug) {\n\t\t\tif (!SLUG_PATTERN.test(input.slug)) {\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: \"slug must only contain lowercase letters, numbers, and hyphens\",\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Check if new slug already exists\n\t\t\tconst slugExists = await db\n\t\t\t\t.selectFrom(\"_emdash_sections\")\n\t\t\t\t.select(\"id\")\n\t\t\t\t.where(\"slug\", \"=\", input.slug)\n\t\t\t\t.executeTakeFirst();\n\n\t\t\tif (slugExists) {\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: \"CONFLICT\",\n\t\t\t\t\t\tmessage: `Section with slug \"${input.slug}\" already exists`,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\t// Build update object\n\t\tconst updates: Record<string, unknown> = {\n\t\t\tupdated_at: new Date().toISOString(),\n\t\t};\n\n\t\tif (input.slug !== undefined) updates.slug = input.slug;\n\t\tif (input.title !== undefined) updates.title = input.title;\n\t\tif (input.description !== undefined) updates.description = input.description;\n\t\tif (input.keywords !== undefined) updates.keywords = JSON.stringify(input.keywords);\n\t\tif (input.content !== undefined) updates.content = JSON.stringify(input.content);\n\t\tif (input.previewMediaId !== undefined) updates.preview_media_id = input.previewMediaId;\n\n\t\tawait db.updateTable(\"_emdash_sections\").set(updates).where(\"id\", \"=\", existing.id).execute();\n\n\t\tconst section = await getSectionById(existing.id, db);\n\t\tif (!section) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"SECTION_UPDATE_ERROR\", message: \"Failed to fetch updated section\" },\n\t\t\t};\n\t\t}\n\n\t\treturn { success: true, data: section };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"SECTION_UPDATE_ERROR\", message: \"Failed to update section\" },\n\t\t};\n\t}\n}\n\n/**\n * Delete a section by slug\n */\nexport async function handleSectionDelete(\n\tdb: Kysely<Database>,\n\tslug: string,\n): Promise<ApiResult<{ deleted: true }>> {\n\ttry {\n\t\t// Check if section exists and get source\n\t\tconst existing = await db\n\t\t\t.selectFrom(\"_emdash_sections\")\n\t\t\t.select([\"id\", \"source\", \"theme_id\"])\n\t\t\t.where(\"slug\", \"=\", slug)\n\t\t\t.executeTakeFirst();\n\n\t\tif (!existing) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: `Section \"${slug}\" not found` },\n\t\t\t};\n\t\t}\n\n\t\t// Prevent deleting theme sections\n\t\tif (existing.source === \"theme\") {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"FORBIDDEN\",\n\t\t\t\t\tmessage:\n\t\t\t\t\t\t\"Cannot delete theme-provided sections. Edit the section to create a user copy, then delete that.\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tawait db.deleteFrom(\"_emdash_sections\").where(\"id\", \"=\", existing.id).execute();\n\n\t\treturn { success: true, data: { deleted: true } };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"SECTION_DELETE_ERROR\", message: \"Failed to delete section\" },\n\t\t};\n\t}\n}\n"],"mappings":"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"}
|
package/dist/seed/index.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import "../types-
|
|
2
|
-
import { _ as SeedTaxonomyTerm, a as applySeed, b as ValidationResult, c as SeedCollection, d as SeedFile, f as SeedMenu, g as SeedTaxonomy, h as SeedSection, i as defaultSeed, l as SeedContentEntry, m as SeedRedirect, n as loadSeed, o as SeedApplyOptions, p as SeedMenuItem, r as loadUserSeed, s as SeedApplyResult, t as validateSeed, u as SeedField, v as SeedWidget, y as SeedWidgetArea } from "../validate-
|
|
1
|
+
import "../types-DQIDRKF2.mjs";
|
|
2
|
+
import { _ as SeedTaxonomyTerm, a as applySeed, b as ValidationResult, c as SeedCollection, d as SeedFile, f as SeedMenu, g as SeedTaxonomy, h as SeedSection, i as defaultSeed, l as SeedContentEntry, m as SeedRedirect, n as loadSeed, o as SeedApplyOptions, p as SeedMenuItem, r as loadUserSeed, s as SeedApplyResult, t as validateSeed, u as SeedField, v as SeedWidget, y as SeedWidgetArea } from "../validate-CTqt5nuE.mjs";
|
|
3
3
|
export { type SeedApplyOptions, type SeedApplyResult, type SeedCollection, type SeedContentEntry, type SeedField, type SeedFile, type SeedMenu, type SeedMenuItem, type SeedRedirect, type SeedSection, type SeedTaxonomy, type SeedTaxonomyTerm, type SeedWidget, type SeedWidgetArea, type ValidationResult, applySeed, defaultSeed, loadSeed, loadUserSeed, validateSeed };
|
package/dist/seed/index.mjs
CHANGED
|
@@ -2,30 +2,30 @@ import "../dialect-helpers-BSbfaO3F.mjs";
|
|
|
2
2
|
import "../config-C3EBstHi.mjs";
|
|
3
3
|
import "../after-CBU2vkoO.mjs";
|
|
4
4
|
import "../object-cache-71tPfV4A.mjs";
|
|
5
|
-
import "../content-
|
|
5
|
+
import "../content-ZHCc1ngN.mjs";
|
|
6
6
|
import "../slugify-B994zkXC.mjs";
|
|
7
7
|
import "../base64-CvLhUKQv.mjs";
|
|
8
8
|
import "../types-CQPWhPxE.mjs";
|
|
9
9
|
import "../media-CzhAn2--.mjs";
|
|
10
|
-
import "../taxonomy-
|
|
10
|
+
import "../taxonomy-BqCZ1TdU.mjs";
|
|
11
11
|
import "../options-CBXIHUoT.mjs";
|
|
12
12
|
import "../redirect-COsS6SWJ.mjs";
|
|
13
13
|
import "../request-cache-BSUptuJR.mjs";
|
|
14
14
|
import "../byline-registry-DDun8sxh.mjs";
|
|
15
|
-
import "../field-defs-cache-
|
|
16
|
-
import "../byline-
|
|
15
|
+
import "../field-defs-cache-BzwBlJrR.mjs";
|
|
16
|
+
import "../byline-yjvWq4ip.mjs";
|
|
17
17
|
import "../dist-D_6CKR8z.mjs";
|
|
18
|
-
import "../fts-manager-
|
|
19
|
-
import "../loader-
|
|
20
|
-
import "../content-refresh-
|
|
21
|
-
import "../collection-deletion-
|
|
22
|
-
import "../registry-
|
|
23
|
-
import "../settings-
|
|
18
|
+
import "../fts-manager-DyZEohZC.mjs";
|
|
19
|
+
import "../loader-CdG_Ci3H.mjs";
|
|
20
|
+
import "../content-refresh-B_8SJzuU.mjs";
|
|
21
|
+
import "../collection-deletion-DQPvMD8R.mjs";
|
|
22
|
+
import "../registry-Cqa2I749.mjs";
|
|
23
|
+
import "../settings-xRdoUZr0.mjs";
|
|
24
24
|
import "../ssrf-Cpvg_n3A.mjs";
|
|
25
25
|
import "../ssrf-DpXgKsna.mjs";
|
|
26
|
-
import { t as validateSeed } from "../validate-
|
|
27
|
-
import { t as applySeed } from "../apply-
|
|
26
|
+
import { t as validateSeed } from "../validate-BMLBvxOC.mjs";
|
|
27
|
+
import { t as applySeed } from "../apply-kUDt80-G.mjs";
|
|
28
28
|
import { t as defaultSeed } from "../default-DdDl3fxv.mjs";
|
|
29
|
-
import { n as loadUserSeed, t as loadSeed } from "../load-
|
|
29
|
+
import { n as loadUserSeed, t as loadSeed } from "../load-YJ2xl3ra.mjs";
|
|
30
30
|
|
|
31
31
|
export { applySeed, defaultSeed, loadSeed, loadUserSeed, validateSeed };
|
package/dist/seo/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as ContentSeo } from "../types-
|
|
2
|
-
import "../types-
|
|
3
|
-
import { i as getHreflangAlternatesWithDb, n as HreflangOptions, r as getHreflangAlternates, t as HreflangAlternate } from "../hreflang-
|
|
1
|
+
import { a as ContentSeo } from "../types-DN64_zzZ.mjs";
|
|
2
|
+
import "../types-DQIDRKF2.mjs";
|
|
3
|
+
import { i as getHreflangAlternatesWithDb, n as HreflangOptions, r as getHreflangAlternates, t as HreflangAlternate } from "../hreflang-DwT2E0Sa.mjs";
|
|
4
4
|
|
|
5
5
|
//#region src/seo/index.d.ts
|
|
6
6
|
/**
|
package/dist/seo/index.mjs
CHANGED
|
@@ -2,23 +2,23 @@ import "../dialect-helpers-BSbfaO3F.mjs";
|
|
|
2
2
|
import "../config-C3EBstHi.mjs";
|
|
3
3
|
import "../after-CBU2vkoO.mjs";
|
|
4
4
|
import "../object-cache-71tPfV4A.mjs";
|
|
5
|
-
import "../content-
|
|
5
|
+
import "../content-ZHCc1ngN.mjs";
|
|
6
6
|
import "../slugify-B994zkXC.mjs";
|
|
7
7
|
import "../base64-CvLhUKQv.mjs";
|
|
8
8
|
import "../types-CQPWhPxE.mjs";
|
|
9
9
|
import "../media-CzhAn2--.mjs";
|
|
10
10
|
import "../options-CBXIHUoT.mjs";
|
|
11
11
|
import "../request-cache-BSUptuJR.mjs";
|
|
12
|
-
import "../fts-manager-
|
|
12
|
+
import "../fts-manager-DyZEohZC.mjs";
|
|
13
13
|
import "../resolve-DOhVEvty.mjs";
|
|
14
|
-
import "../loader-
|
|
15
|
-
import "../content-refresh-
|
|
16
|
-
import "../collection-deletion-
|
|
17
|
-
import "../registry-
|
|
18
|
-
import "../query-
|
|
19
|
-
import "../settings-
|
|
14
|
+
import "../loader-CdG_Ci3H.mjs";
|
|
15
|
+
import "../content-refresh-B_8SJzuU.mjs";
|
|
16
|
+
import "../collection-deletion-DQPvMD8R.mjs";
|
|
17
|
+
import "../registry-Cqa2I749.mjs";
|
|
18
|
+
import "../query-BWs7O5Il.mjs";
|
|
19
|
+
import "../settings-xRdoUZr0.mjs";
|
|
20
20
|
import { t as buildSeoImageUrl } from "../media-url-BvoX88FA.mjs";
|
|
21
|
-
import { n as getHreflangAlternatesWithDb, t as getHreflangAlternates } from "../hreflang-
|
|
21
|
+
import { n as getHreflangAlternatesWithDb, t as getHreflangAlternates } from "../hreflang-BBe83kKa.mjs";
|
|
22
22
|
|
|
23
23
|
//#region src/seo/index.ts
|
|
24
24
|
const TRAILING_SLASH_RE = /\/$/;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { c as invalidateCollectionCache } from "./object-cache-71tPfV4A.mjs";
|
|
2
|
-
import { n as chunks, t as SQL_BATCH_SIZE } from "./chunks-
|
|
2
|
+
import { n as chunks, t as SQL_BATCH_SIZE } from "./chunks-fiZ8G9RJ.mjs";
|
|
3
3
|
import { sql } from "kysely";
|
|
4
4
|
|
|
5
5
|
//#region src/database/repositories/seo.ts
|
|
@@ -130,4 +130,4 @@ var SeoRepository = class {
|
|
|
130
130
|
|
|
131
131
|
//#endregion
|
|
132
132
|
export { SeoRepository as t };
|
|
133
|
-
//# sourceMappingURL=seo-
|
|
133
|
+
//# sourceMappingURL=seo-Dq4G-RrO.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seo-
|
|
1
|
+
{"version":3,"file":"seo-Dq4G-RrO.mjs","names":[],"sources":["../src/database/repositories/seo.ts"],"sourcesContent":["import { sql, type Kysely } from \"kysely\";\n\nimport { invalidateCollectionCache } from \"../../object-cache/index.js\";\nimport { chunks, SQL_BATCH_SIZE } from \"../../utils/chunks.js\";\nimport type { Database } from \"../types.js\";\nimport type { ContentSeo, ContentSeoInput } from \"./types.js\";\n\n/** Default SEO values for content without an explicit SEO row */\nconst SEO_DEFAULTS: ContentSeo = {\n\ttitle: null,\n\tdescription: null,\n\timage: null,\n\tcanonical: null,\n\tnoIndex: false,\n};\n\n/**\n * Returns true if the input has at least one explicitly-set SEO field.\n * Used to skip no-op upserts when callers pass `{ seo: {} }`.\n */\nfunction hasAnyField(input: ContentSeoInput): boolean {\n\treturn (\n\t\tinput.title !== undefined ||\n\t\tinput.description !== undefined ||\n\t\tinput.image !== undefined ||\n\t\tinput.canonical !== undefined ||\n\t\tinput.noIndex !== undefined\n\t);\n}\n\n/**\n * Repository for SEO metadata stored in `_emdash_seo`.\n *\n * SEO data lives in a separate table keyed by (collection, content_id).\n * Only collections with `has_seo = 1` should use this — callers are\n * responsible for checking the flag before reading/writing.\n */\nexport class SeoRepository {\n\tconstructor(private db: Kysely<Database>) {}\n\n\t/**\n\t * Check whether a collection has SEO enabled (`has_seo = 1`).\n\t * Returns `false` if the collection does not exist.\n\t */\n\tasync isEnabled(collection: string): Promise<boolean> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_emdash_collections\")\n\t\t\t.select(\"has_seo\")\n\t\t\t.where(\"slug\", \"=\", collection)\n\t\t\t.executeTakeFirst();\n\t\treturn row?.has_seo === 1;\n\t}\n\n\t/**\n\t * Get SEO data for a content item. Returns null defaults if no row exists.\n\t */\n\tasync get(collection: string, contentId: string): Promise<ContentSeo> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_emdash_seo\")\n\t\t\t.selectAll()\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"content_id\", \"=\", contentId)\n\t\t\t.executeTakeFirst();\n\n\t\tif (!row) {\n\t\t\treturn { ...SEO_DEFAULTS };\n\t\t}\n\n\t\treturn {\n\t\t\ttitle: row.seo_title ?? null,\n\t\t\tdescription: row.seo_description ?? null,\n\t\t\timage: row.seo_image ?? null,\n\t\t\tcanonical: row.seo_canonical ?? null,\n\t\t\tnoIndex: row.seo_no_index === 1,\n\t\t};\n\t}\n\n\t/**\n\t * Get SEO data for multiple content items.\n\t * Returns a Map keyed by content_id. Items without SEO rows get defaults.\n\t *\n\t * Chunks the `content_id IN (…)` clause so the total bound-parameter count\n\t * per statement (ids + the `collection = ?` filter) stays within Cloudflare\n\t * D1's 100-variable limit regardless of how many content items are passed.\n\t */\n\tasync getMany(collection: string, contentIds: string[]): Promise<Map<string, ContentSeo>> {\n\t\tconst result = new Map<string, ContentSeo>();\n\n\t\tif (contentIds.length === 0) return result;\n\n\t\t// Pre-fill with defaults so every input id has an entry even if no row exists.\n\t\tfor (const id of contentIds) {\n\t\t\tresult.set(id, { ...SEO_DEFAULTS });\n\t\t}\n\n\t\tconst uniqueContentIds = [...new Set(contentIds)];\n\t\tfor (const chunk of chunks(uniqueContentIds, SQL_BATCH_SIZE)) {\n\t\t\tconst rows = await this.db\n\t\t\t\t.selectFrom(\"_emdash_seo\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t\t.where(\"content_id\", \"in\", chunk)\n\t\t\t\t.execute();\n\n\t\t\tfor (const row of rows) {\n\t\t\t\tresult.set(row.content_id, {\n\t\t\t\t\ttitle: row.seo_title ?? null,\n\t\t\t\t\tdescription: row.seo_description ?? null,\n\t\t\t\t\timage: row.seo_image ?? null,\n\t\t\t\t\tcanonical: row.seo_canonical ?? null,\n\t\t\t\t\tnoIndex: row.seo_no_index === 1,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Upsert SEO data for a content item using INSERT ON CONFLICT DO UPDATE\n\t * for atomicity. Skips no-op writes when input has no fields set.\n\t */\n\tasync upsert(collection: string, contentId: string, input: ContentSeoInput): Promise<ContentSeo> {\n\t\t// Skip no-op: empty input (e.g., `{ seo: {} }` from form libs)\n\t\tif (!hasAnyField(input)) {\n\t\t\treturn this.get(collection, contentId);\n\t\t}\n\n\t\tconst now = new Date().toISOString();\n\n\t\t// Use INSERT ON CONFLICT for atomic upsert — avoids TOCTOU race\n\t\t// where two concurrent requests both see \"no row\" and both try INSERT.\n\t\t//\n\t\t// On conflict, we use COALESCE(excluded.col, current.col) so that\n\t\t// only explicitly-provided fields overwrite existing values.\n\t\tawait sql`\n\t\t\tINSERT INTO _emdash_seo (\n\t\t\t\tcollection, content_id,\n\t\t\t\tseo_title, seo_description, seo_image, seo_canonical, seo_no_index,\n\t\t\t\tcreated_at, updated_at\n\t\t\t) VALUES (\n\t\t\t\t${collection}, ${contentId},\n\t\t\t\t${input.title ?? null}, ${input.description ?? null},\n\t\t\t\t${input.image ?? null}, ${input.canonical ?? null},\n\t\t\t\t${input.noIndex ? 1 : 0},\n\t\t\t\t${now}, ${now}\n\t\t\t)\n\t\t\tON CONFLICT (collection, content_id) DO UPDATE SET\n\t\t\t\tseo_title = ${input.title !== undefined ? sql`${input.title}` : sql`_emdash_seo.seo_title`},\n\t\t\t\tseo_description = ${input.description !== undefined ? sql`${input.description}` : sql`_emdash_seo.seo_description`},\n\t\t\t\tseo_image = ${input.image !== undefined ? sql`${input.image}` : sql`_emdash_seo.seo_image`},\n\t\t\t\tseo_canonical = ${input.canonical !== undefined ? sql`${input.canonical}` : sql`_emdash_seo.seo_canonical`},\n\t\t\t\tseo_no_index = ${input.noIndex !== undefined ? sql`${input.noIndex ? 1 : 0}` : sql`_emdash_seo.seo_no_index`},\n\t\t\t\tupdated_at = ${now}\n\t\t`.execute(this.db);\n\n\t\tinvalidateCollectionCache(collection);\n\t\treturn this.get(collection, contentId);\n\t}\n\n\t/**\n\t * Delete SEO data for a content item.\n\t */\n\tasync delete(collection: string, contentId: string): Promise<void> {\n\t\tawait this.db\n\t\t\t.deleteFrom(\"_emdash_seo\")\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"content_id\", \"=\", contentId)\n\t\t\t.execute();\n\t\tinvalidateCollectionCache(collection);\n\t}\n\n\t/**\n\t * Copy SEO data from one content item to another.\n\t * Used by duplicate. Clears canonical (it pointed to the original).\n\t */\n\tasync copyForDuplicate(collection: string, sourceId: string, targetId: string): Promise<void> {\n\t\tconst source = await this.get(collection, sourceId);\n\n\t\t// Only write if there's actual SEO data worth copying\n\t\tif (\n\t\t\tsource.title !== null ||\n\t\t\tsource.description !== null ||\n\t\t\tsource.image !== null ||\n\t\t\tsource.noIndex\n\t\t) {\n\t\t\tawait this.upsert(collection, targetId, {\n\t\t\t\ttitle: source.title,\n\t\t\t\tdescription: source.description,\n\t\t\t\timage: source.image,\n\t\t\t\tcanonical: null, // Don't copy canonical — it pointed to the original\n\t\t\t\tnoIndex: source.noIndex,\n\t\t\t});\n\t\t}\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as validateIdentifier } from "./validate-BpwoDOgt.mjs";
|
|
2
2
|
import { t as CommentRepository } from "./comment-Dxlep6cE.mjs";
|
|
3
|
-
import { t as escapeHtml } from "./escape-
|
|
3
|
+
import { t as escapeHtml } from "./escape-DUY2r2Zs.mjs";
|
|
4
4
|
|
|
5
5
|
//#region src/comments/notifications.ts
|
|
6
6
|
const NOTIFICATION_SOURCE = "emdash-comments";
|
|
@@ -192,4 +192,4 @@ function commentToStored(comment) {
|
|
|
192
192
|
|
|
193
193
|
//#endregion
|
|
194
194
|
export { sendCommentNotification as i, moderateComment as n, lookupContentAuthor as r, createComment as t };
|
|
195
|
-
//# sourceMappingURL=service-
|
|
195
|
+
//# sourceMappingURL=service-BpArayPr.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service-xMiMSTsR.mjs","names":[],"sources":["../src/comments/notifications.ts","../src/comments/service.ts"],"sourcesContent":["/**\n * Comment Notification Emails\n *\n * Sends email notifications to content authors when comments are\n * approved on their content. Used by:\n * - Public comment POST route (comment:afterCreate, if auto-approved)\n * - Admin moderation route (comment:afterModerate, when approving)\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { escapeHtml } from \"../api/escape.js\";\nimport type { Database } from \"../database/types.js\";\nimport { validateIdentifier } from \"../database/validate.js\";\nimport type { EmailPipeline } from \"../plugins/email.js\";\nimport type { EmailMessage } from \"../plugins/types.js\";\n\nconst NOTIFICATION_SOURCE = \"emdash-comments\";\nconst MAX_EXCERPT_LENGTH = 500;\nconst CRLF_RE = /[\\r\\n]/g;\n\nexport interface CommentNotificationData {\n\tcommentAuthorName: string;\n\tcommentBody: string;\n\tcontentTitle: string;\n\tcollection: string;\n\tadminBaseUrl: string;\n}\n\n/**\n * Build an email notification for a new comment.\n */\nexport function buildCommentNotificationEmail(\n\tto: string,\n\tdata: CommentNotificationData,\n): EmailMessage {\n\tconst title = data.contentTitle || `${data.collection} item`;\n\tconst subject = `New comment on \"${title}\"`.replace(CRLF_RE, \" \");\n\n\tconst excerpt =\n\t\tdata.commentBody.length > MAX_EXCERPT_LENGTH\n\t\t\t? data.commentBody.slice(0, MAX_EXCERPT_LENGTH) + \"...\"\n\t\t\t: data.commentBody;\n\n\tconst adminUrl = `${data.adminBaseUrl}/admin/comments`;\n\n\tconst text = [\n\t\t`${data.commentAuthorName} commented on \"${title}\":`,\n\t\t\"\",\n\t\texcerpt,\n\t\t\"\",\n\t\t`View in admin: ${adminUrl}`,\n\t].join(\"\\n\");\n\n\tconst html = [\n\t\t`<p><strong>${escapeHtml(data.commentAuthorName)}</strong> commented on “${escapeHtml(title)}”:</p>`,\n\t\t`<blockquote style=\"border-left:3px solid #ccc;padding-left:12px;margin:12px 0;color:#555\">${escapeHtml(excerpt)}</blockquote>`,\n\t\t`<p><a href=\"${escapeHtml(adminUrl)}\">View in admin</a></p>`,\n\t].join(\"\\n\");\n\n\treturn { to, subject, text, html };\n}\n\n/**\n * Send a comment notification to the content author if all conditions are met:\n * 1. Comment status is \"approved\"\n * 2. Content author exists and has an email\n * 3. Email provider is configured\n * 4. Commenter is not the content author (no self-notifications)\n *\n * Returns true if the email was sent, false if skipped.\n */\nexport async function sendCommentNotification(params: {\n\temail: EmailPipeline;\n\tcomment: {\n\t\tauthorName: string;\n\t\tauthorEmail: string;\n\t\tbody: string;\n\t\tstatus: string;\n\t\tcollection: string;\n\t};\n\tcontentTitle?: string;\n\tcontentAuthor?: { email: string; name: string | null };\n\tadminBaseUrl: string;\n}): Promise<boolean> {\n\tconst { email, comment, contentAuthor, adminBaseUrl } = params;\n\n\tif (comment.status !== \"approved\") return false;\n\tif (!contentAuthor?.email) return false;\n\tif (!email.isAvailable()) return false;\n\tif (comment.authorEmail.toLowerCase() === contentAuthor.email.toLowerCase()) return false;\n\n\tconst message = buildCommentNotificationEmail(contentAuthor.email, {\n\t\tcommentAuthorName: comment.authorName,\n\t\tcommentBody: comment.body,\n\t\tcontentTitle: params.contentTitle || \"\",\n\t\tcollection: comment.collection,\n\t\tadminBaseUrl,\n\t});\n\n\tawait email.send(message, NOTIFICATION_SOURCE);\n\treturn true;\n}\n\n/**\n * Look up a content item's author from the database.\n *\n * Used by the admin moderation route where content info isn't\n * readily available (only the comment record is at hand).\n */\nexport async function lookupContentAuthor(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tcontentId: string,\n): Promise<{\n\tslug: string;\n\tauthor?: { id: string; email: string; name: string | null };\n} | null> {\n\tvalidateIdentifier(collection, \"collection\");\n\n\tconst contentRow = await db\n\t\t.selectFrom(`ec_${collection}` as never)\n\t\t.select([\"slug\" as never, \"author_id\" as never])\n\t\t.where(\"id\" as never, \"=\", contentId as never)\n\t\t.executeTakeFirst();\n\n\tif (!contentRow) return null;\n\n\tconst typed = contentRow as { slug: string; author_id: string | null };\n\n\tlet author: { id: string; email: string; name: string | null } | undefined;\n\tif (typed.author_id) {\n\t\tconst userRow = await db\n\t\t\t.selectFrom(\"users\")\n\t\t\t.select([\"id\", \"name\", \"email\", \"email_verified\"])\n\t\t\t.where(\"id\", \"=\", typed.author_id)\n\t\t\t.executeTakeFirst();\n\t\tif (userRow && userRow.email_verified) {\n\t\t\tauthor = { id: userRow.id, email: userRow.email, name: userRow.name };\n\t\t}\n\t}\n\n\treturn { slug: typed.slug, author };\n}\n","/**\n * Comment Service\n *\n * Orchestrates comment creation through the hook pipeline:\n * 1. Run comment:beforeCreate pipeline (transform/reject)\n * 2. Query priorApprovedCount for first-time moderation\n * 3. Invoke comment:moderate exclusive hook (or built-in fallback)\n * 4. Save comment with determined status\n * 5. Fire comment:afterCreate (fire-and-forget)\n *\n * Also handles admin moderation (status changes) with afterModerate hooks.\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { CommentRepository } from \"../database/repositories/comment.js\";\nimport type { Comment, CommentStatus } from \"../database/repositories/comment.js\";\nimport type { Database } from \"../database/types.js\";\nimport type {\n\tCollectionCommentSettings,\n\tCommentAfterCreateEvent,\n\tCommentAfterModerateEvent,\n\tCommentBeforeCreateEvent,\n\tCommentModerateEvent,\n\tModerationDecision,\n\tStoredComment,\n} from \"../plugins/types.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface CommentCreateInput {\n\tcollection: string;\n\tcontentId: string;\n\tparentId?: string | null;\n\tauthorName: string;\n\tauthorEmail: string;\n\tauthorUserId?: string | null;\n\tbody: string;\n\tipHash?: string | null;\n\tuserAgent?: string | null;\n}\n\nexport interface CommentCreateResult {\n\tcomment: Comment;\n\tdecision: ModerationDecision;\n}\n\n/**\n * Hook runner interface — injected from the runtime so the service\n * doesn't need to know about the hook pipeline internals.\n */\nexport interface CommentHookRunner {\n\t/** Run comment:beforeCreate pipeline. Returns modified event or false. */\n\trunBeforeCreate(event: CommentBeforeCreateEvent): Promise<CommentBeforeCreateEvent | false>;\n\n\t/** Run comment:moderate exclusive hook. Returns moderation decision. */\n\trunModerate(event: CommentModerateEvent): Promise<ModerationDecision>;\n\n\t/** Fire comment:afterCreate (fire-and-forget). */\n\tfireAfterCreate(event: CommentAfterCreateEvent): void;\n\n\t/** Fire comment:afterModerate (fire-and-forget). */\n\tfireAfterModerate(event: CommentAfterModerateEvent): void;\n}\n\n// ---------------------------------------------------------------------------\n// Service\n// ---------------------------------------------------------------------------\n\n/**\n * Create a comment through the full hook pipeline.\n *\n * Returns null if the comment was rejected by a beforeCreate handler.\n */\nexport async function createComment(\n\tdb: Kysely<Database>,\n\tinput: CommentCreateInput,\n\tcollectionSettings: CollectionCommentSettings,\n\thooks: CommentHookRunner,\n\tcontentInfo?: {\n\t\tid: string;\n\t\tcollection: string;\n\t\tslug: string;\n\t\ttitle?: string;\n\t\tauthor?: { id: string; name: string | null; email: string };\n\t},\n): Promise<CommentCreateResult | null> {\n\tconst repo = new CommentRepository(db);\n\n\t// 1. Build the beforeCreate event\n\tconst beforeCreateEvent: CommentBeforeCreateEvent = {\n\t\tcomment: {\n\t\t\tcollection: input.collection,\n\t\t\tcontentId: input.contentId,\n\t\t\tparentId: input.parentId ?? null,\n\t\t\tauthorName: input.authorName,\n\t\t\tauthorEmail: input.authorEmail,\n\t\t\tauthorUserId: input.authorUserId ?? null,\n\t\t\tbody: input.body,\n\t\t\tipHash: input.ipHash ?? null,\n\t\t\tuserAgent: input.userAgent ?? null,\n\t\t},\n\t\tmetadata: {},\n\t};\n\n\t// 2. Run comment:beforeCreate pipeline\n\tconst result = await hooks.runBeforeCreate(beforeCreateEvent);\n\tif (result === false) {\n\t\treturn null; // Rejected\n\t}\n\n\tconst event = result;\n\n\t// 3. Query prior approved count for first-time moderation\n\tconst priorApprovedCount = await repo.countApprovedByEmail(event.comment.authorEmail);\n\n\t// 4. Run comment:moderate exclusive hook\n\tconst moderateEvent: CommentModerateEvent = {\n\t\tcomment: event.comment,\n\t\tmetadata: event.metadata,\n\t\tcollectionSettings,\n\t\tpriorApprovedCount,\n\t};\n\n\tconst decision = await hooks.runModerate(moderateEvent);\n\n\t// 5. Save comment with determined status\n\tconst comment = await repo.create({\n\t\tcollection: event.comment.collection,\n\t\tcontentId: event.comment.contentId,\n\t\tparentId: event.comment.parentId,\n\t\tauthorName: event.comment.authorName,\n\t\tauthorEmail: event.comment.authorEmail,\n\t\tauthorUserId: event.comment.authorUserId,\n\t\tbody: event.comment.body,\n\t\tstatus: decision.status,\n\t\tipHash: event.comment.ipHash,\n\t\tuserAgent: event.comment.userAgent,\n\t\tmoderationMetadata: Object.keys(event.metadata).length > 0 ? event.metadata : null,\n\t});\n\n\t// 6. Fire comment:afterCreate (fire-and-forget)\n\tif (contentInfo) {\n\t\tconst afterEvent: CommentAfterCreateEvent = {\n\t\t\tcomment: commentToStored(comment),\n\t\t\tmetadata: event.metadata,\n\t\t\tcontent: {\n\t\t\t\tid: contentInfo.id,\n\t\t\t\tcollection: contentInfo.collection,\n\t\t\t\tslug: contentInfo.slug,\n\t\t\t\ttitle: contentInfo.title,\n\t\t\t},\n\t\t\tcontentAuthor: contentInfo.author,\n\t\t};\n\t\thooks.fireAfterCreate(afterEvent);\n\t}\n\n\treturn { comment, decision };\n}\n\n/**\n * Admin moderation — change a comment's status.\n * Fires comment:afterModerate hook.\n */\nexport async function moderateComment(\n\tdb: Kysely<Database>,\n\tid: string,\n\tnewStatus: CommentStatus,\n\tmoderator: { id: string; name: string | null },\n\thooks: CommentHookRunner,\n): Promise<Comment | null> {\n\tconst repo = new CommentRepository(db);\n\tconst existing = await repo.findById(id);\n\tif (!existing) return null;\n\n\tconst previousStatus = existing.status;\n\tconst updated = await repo.updateStatus(id, newStatus);\n\tif (!updated) return null;\n\n\t// Fire comment:afterModerate (fire-and-forget)\n\tconst afterEvent: CommentAfterModerateEvent = {\n\t\tcomment: commentToStored(updated),\n\t\tpreviousStatus,\n\t\tnewStatus,\n\t\tmoderator,\n\t};\n\thooks.fireAfterModerate(afterEvent);\n\n\treturn updated;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction commentToStored(comment: Comment): StoredComment {\n\treturn {\n\t\tid: comment.id,\n\t\tcollection: comment.collection,\n\t\tcontentId: comment.contentId,\n\t\tparentId: comment.parentId,\n\t\tauthorName: comment.authorName,\n\t\tauthorEmail: comment.authorEmail,\n\t\tauthorUserId: comment.authorUserId,\n\t\tbody: comment.body,\n\t\tstatus: comment.status,\n\t\tmoderationMetadata: comment.moderationMetadata,\n\t\tcreatedAt: comment.createdAt,\n\t\tupdatedAt: comment.updatedAt,\n\t};\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"service-BpArayPr.mjs","names":[],"sources":["../src/comments/notifications.ts","../src/comments/service.ts"],"sourcesContent":["/**\n * Comment Notification Emails\n *\n * Sends email notifications to content authors when comments are\n * approved on their content. Used by:\n * - Public comment POST route (comment:afterCreate, if auto-approved)\n * - Admin moderation route (comment:afterModerate, when approving)\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { escapeHtml } from \"../api/escape.js\";\nimport type { Database } from \"../database/types.js\";\nimport { validateIdentifier } from \"../database/validate.js\";\nimport type { EmailPipeline } from \"../plugins/email.js\";\nimport type { EmailMessage } from \"../plugins/types.js\";\n\nconst NOTIFICATION_SOURCE = \"emdash-comments\";\nconst MAX_EXCERPT_LENGTH = 500;\nconst CRLF_RE = /[\\r\\n]/g;\n\nexport interface CommentNotificationData {\n\tcommentAuthorName: string;\n\tcommentBody: string;\n\tcontentTitle: string;\n\tcollection: string;\n\tadminBaseUrl: string;\n}\n\n/**\n * Build an email notification for a new comment.\n */\nexport function buildCommentNotificationEmail(\n\tto: string,\n\tdata: CommentNotificationData,\n): EmailMessage {\n\tconst title = data.contentTitle || `${data.collection} item`;\n\tconst subject = `New comment on \"${title}\"`.replace(CRLF_RE, \" \");\n\n\tconst excerpt =\n\t\tdata.commentBody.length > MAX_EXCERPT_LENGTH\n\t\t\t? data.commentBody.slice(0, MAX_EXCERPT_LENGTH) + \"...\"\n\t\t\t: data.commentBody;\n\n\tconst adminUrl = `${data.adminBaseUrl}/admin/comments`;\n\n\tconst text = [\n\t\t`${data.commentAuthorName} commented on \"${title}\":`,\n\t\t\"\",\n\t\texcerpt,\n\t\t\"\",\n\t\t`View in admin: ${adminUrl}`,\n\t].join(\"\\n\");\n\n\tconst html = [\n\t\t`<p><strong>${escapeHtml(data.commentAuthorName)}</strong> commented on “${escapeHtml(title)}”:</p>`,\n\t\t`<blockquote style=\"border-left:3px solid #ccc;padding-left:12px;margin:12px 0;color:#555\">${escapeHtml(excerpt)}</blockquote>`,\n\t\t`<p><a href=\"${escapeHtml(adminUrl)}\">View in admin</a></p>`,\n\t].join(\"\\n\");\n\n\treturn { to, subject, text, html };\n}\n\n/**\n * Send a comment notification to the content author if all conditions are met:\n * 1. Comment status is \"approved\"\n * 2. Content author exists and has an email\n * 3. Email provider is configured\n * 4. Commenter is not the content author (no self-notifications)\n *\n * Returns true if the email was sent, false if skipped.\n */\nexport async function sendCommentNotification(params: {\n\temail: EmailPipeline;\n\tcomment: {\n\t\tauthorName: string;\n\t\tauthorEmail: string;\n\t\tbody: string;\n\t\tstatus: string;\n\t\tcollection: string;\n\t};\n\tcontentTitle?: string;\n\tcontentAuthor?: { email: string; name: string | null };\n\tadminBaseUrl: string;\n}): Promise<boolean> {\n\tconst { email, comment, contentAuthor, adminBaseUrl } = params;\n\n\tif (comment.status !== \"approved\") return false;\n\tif (!contentAuthor?.email) return false;\n\tif (!email.isAvailable()) return false;\n\tif (comment.authorEmail.toLowerCase() === contentAuthor.email.toLowerCase()) return false;\n\n\tconst message = buildCommentNotificationEmail(contentAuthor.email, {\n\t\tcommentAuthorName: comment.authorName,\n\t\tcommentBody: comment.body,\n\t\tcontentTitle: params.contentTitle || \"\",\n\t\tcollection: comment.collection,\n\t\tadminBaseUrl,\n\t});\n\n\tawait email.send(message, NOTIFICATION_SOURCE);\n\treturn true;\n}\n\n/**\n * Look up a content item's author from the database.\n *\n * Used by the admin moderation route where content info isn't\n * readily available (only the comment record is at hand).\n */\nexport async function lookupContentAuthor(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tcontentId: string,\n): Promise<{\n\tslug: string;\n\tauthor?: { id: string; email: string; name: string | null };\n} | null> {\n\tvalidateIdentifier(collection, \"collection\");\n\n\tconst contentRow = await db\n\t\t.selectFrom(`ec_${collection}` as never)\n\t\t.select([\"slug\" as never, \"author_id\" as never])\n\t\t.where(\"id\" as never, \"=\", contentId as never)\n\t\t.executeTakeFirst();\n\n\tif (!contentRow) return null;\n\n\tconst typed = contentRow as { slug: string; author_id: string | null };\n\n\tlet author: { id: string; email: string; name: string | null } | undefined;\n\tif (typed.author_id) {\n\t\tconst userRow = await db\n\t\t\t.selectFrom(\"users\")\n\t\t\t.select([\"id\", \"name\", \"email\", \"email_verified\"])\n\t\t\t.where(\"id\", \"=\", typed.author_id)\n\t\t\t.executeTakeFirst();\n\t\tif (userRow && userRow.email_verified) {\n\t\t\tauthor = { id: userRow.id, email: userRow.email, name: userRow.name };\n\t\t}\n\t}\n\n\treturn { slug: typed.slug, author };\n}\n","/**\n * Comment Service\n *\n * Orchestrates comment creation through the hook pipeline:\n * 1. Run comment:beforeCreate pipeline (transform/reject)\n * 2. Query priorApprovedCount for first-time moderation\n * 3. Invoke comment:moderate exclusive hook (or built-in fallback)\n * 4. Save comment with determined status\n * 5. Fire comment:afterCreate (fire-and-forget)\n *\n * Also handles admin moderation (status changes) with afterModerate hooks.\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { CommentRepository } from \"../database/repositories/comment.js\";\nimport type { Comment, CommentStatus } from \"../database/repositories/comment.js\";\nimport type { Database } from \"../database/types.js\";\nimport type {\n\tCollectionCommentSettings,\n\tCommentAfterCreateEvent,\n\tCommentAfterModerateEvent,\n\tCommentBeforeCreateEvent,\n\tCommentModerateEvent,\n\tModerationDecision,\n\tStoredComment,\n} from \"../plugins/types.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface CommentCreateInput {\n\tcollection: string;\n\tcontentId: string;\n\tparentId?: string | null;\n\tauthorName: string;\n\tauthorEmail: string;\n\tauthorUserId?: string | null;\n\tbody: string;\n\tipHash?: string | null;\n\tuserAgent?: string | null;\n}\n\nexport interface CommentCreateResult {\n\tcomment: Comment;\n\tdecision: ModerationDecision;\n}\n\n/**\n * Hook runner interface — injected from the runtime so the service\n * doesn't need to know about the hook pipeline internals.\n */\nexport interface CommentHookRunner {\n\t/** Run comment:beforeCreate pipeline. Returns modified event or false. */\n\trunBeforeCreate(event: CommentBeforeCreateEvent): Promise<CommentBeforeCreateEvent | false>;\n\n\t/** Run comment:moderate exclusive hook. Returns moderation decision. */\n\trunModerate(event: CommentModerateEvent): Promise<ModerationDecision>;\n\n\t/** Fire comment:afterCreate (fire-and-forget). */\n\tfireAfterCreate(event: CommentAfterCreateEvent): void;\n\n\t/** Fire comment:afterModerate (fire-and-forget). */\n\tfireAfterModerate(event: CommentAfterModerateEvent): void;\n}\n\n// ---------------------------------------------------------------------------\n// Service\n// ---------------------------------------------------------------------------\n\n/**\n * Create a comment through the full hook pipeline.\n *\n * Returns null if the comment was rejected by a beforeCreate handler.\n */\nexport async function createComment(\n\tdb: Kysely<Database>,\n\tinput: CommentCreateInput,\n\tcollectionSettings: CollectionCommentSettings,\n\thooks: CommentHookRunner,\n\tcontentInfo?: {\n\t\tid: string;\n\t\tcollection: string;\n\t\tslug: string;\n\t\ttitle?: string;\n\t\tauthor?: { id: string; name: string | null; email: string };\n\t},\n): Promise<CommentCreateResult | null> {\n\tconst repo = new CommentRepository(db);\n\n\t// 1. Build the beforeCreate event\n\tconst beforeCreateEvent: CommentBeforeCreateEvent = {\n\t\tcomment: {\n\t\t\tcollection: input.collection,\n\t\t\tcontentId: input.contentId,\n\t\t\tparentId: input.parentId ?? null,\n\t\t\tauthorName: input.authorName,\n\t\t\tauthorEmail: input.authorEmail,\n\t\t\tauthorUserId: input.authorUserId ?? null,\n\t\t\tbody: input.body,\n\t\t\tipHash: input.ipHash ?? null,\n\t\t\tuserAgent: input.userAgent ?? null,\n\t\t},\n\t\tmetadata: {},\n\t};\n\n\t// 2. Run comment:beforeCreate pipeline\n\tconst result = await hooks.runBeforeCreate(beforeCreateEvent);\n\tif (result === false) {\n\t\treturn null; // Rejected\n\t}\n\n\tconst event = result;\n\n\t// 3. Query prior approved count for first-time moderation\n\tconst priorApprovedCount = await repo.countApprovedByEmail(event.comment.authorEmail);\n\n\t// 4. Run comment:moderate exclusive hook\n\tconst moderateEvent: CommentModerateEvent = {\n\t\tcomment: event.comment,\n\t\tmetadata: event.metadata,\n\t\tcollectionSettings,\n\t\tpriorApprovedCount,\n\t};\n\n\tconst decision = await hooks.runModerate(moderateEvent);\n\n\t// 5. Save comment with determined status\n\tconst comment = await repo.create({\n\t\tcollection: event.comment.collection,\n\t\tcontentId: event.comment.contentId,\n\t\tparentId: event.comment.parentId,\n\t\tauthorName: event.comment.authorName,\n\t\tauthorEmail: event.comment.authorEmail,\n\t\tauthorUserId: event.comment.authorUserId,\n\t\tbody: event.comment.body,\n\t\tstatus: decision.status,\n\t\tipHash: event.comment.ipHash,\n\t\tuserAgent: event.comment.userAgent,\n\t\tmoderationMetadata: Object.keys(event.metadata).length > 0 ? event.metadata : null,\n\t});\n\n\t// 6. Fire comment:afterCreate (fire-and-forget)\n\tif (contentInfo) {\n\t\tconst afterEvent: CommentAfterCreateEvent = {\n\t\t\tcomment: commentToStored(comment),\n\t\t\tmetadata: event.metadata,\n\t\t\tcontent: {\n\t\t\t\tid: contentInfo.id,\n\t\t\t\tcollection: contentInfo.collection,\n\t\t\t\tslug: contentInfo.slug,\n\t\t\t\ttitle: contentInfo.title,\n\t\t\t},\n\t\t\tcontentAuthor: contentInfo.author,\n\t\t};\n\t\thooks.fireAfterCreate(afterEvent);\n\t}\n\n\treturn { comment, decision };\n}\n\n/**\n * Admin moderation — change a comment's status.\n * Fires comment:afterModerate hook.\n */\nexport async function moderateComment(\n\tdb: Kysely<Database>,\n\tid: string,\n\tnewStatus: CommentStatus,\n\tmoderator: { id: string; name: string | null },\n\thooks: CommentHookRunner,\n): Promise<Comment | null> {\n\tconst repo = new CommentRepository(db);\n\tconst existing = await repo.findById(id);\n\tif (!existing) return null;\n\n\tconst previousStatus = existing.status;\n\tconst updated = await repo.updateStatus(id, newStatus);\n\tif (!updated) return null;\n\n\t// Fire comment:afterModerate (fire-and-forget)\n\tconst afterEvent: CommentAfterModerateEvent = {\n\t\tcomment: commentToStored(updated),\n\t\tpreviousStatus,\n\t\tnewStatus,\n\t\tmoderator,\n\t};\n\thooks.fireAfterModerate(afterEvent);\n\n\treturn updated;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction commentToStored(comment: Comment): StoredComment {\n\treturn {\n\t\tid: comment.id,\n\t\tcollection: comment.collection,\n\t\tcontentId: comment.contentId,\n\t\tparentId: comment.parentId,\n\t\tauthorName: comment.authorName,\n\t\tauthorEmail: comment.authorEmail,\n\t\tauthorUserId: comment.authorUserId,\n\t\tbody: comment.body,\n\t\tstatus: comment.status,\n\t\tmoderationMetadata: comment.moderationMetadata,\n\t\tcreatedAt: comment.createdAt,\n\t\tupdatedAt: comment.updatedAt,\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-user-
|
|
1
|
+
{"version":3,"file":"session-user-BEorKVT-.mjs","names":[],"sources":["../src/astro/session-user.ts"],"sourcesContent":["import { after } from \"../after.js\";\n\n/**\n * Backstop timeout for resolving the session user. A live session-store read\n * settles in a few ms, so this only ever fires on a genuinely stalled read.\n */\nexport const SESSION_GET_TIMEOUT_MS = 3_000;\n\n/**\n * Resolve the Astro session user without risking an isolate-wide hang.\n *\n * On Cloudflare Workers, a request cancelled mid-`session.get()` (client\n * disconnect, context teardown) can leave the underlying session-store read as\n * a promise that never settles — neither resolving nor rejecting. Awaiting it\n * directly hangs the request, and because the stalled promise is shared at the\n * isolate level, every later session-bearing request hangs too (observed as\n * 0-CPU, multi-minute, `canceled` responses; see #1274). A surrounding\n * try/catch cannot help: the promise never rejects.\n *\n * Two layers, mirroring the reclaimable-cache pattern used elsewhere in core:\n * 1. `after()` anchors the read so a cancelled request still drives it to\n * completion — the promise settles and the isolate is not poisoned for\n * subsequent requests (prevents the hang rather than merely surviving it).\n * 2. A timeout is a fail-closed backstop: a still-stalled (or rejecting) read\n * resolves to `undefined`, and every caller treats the absence of a session\n * user as unauthenticated (anonymous on public routes, 401/redirect on\n * protected ones). It can only ever drop privileges for that one request,\n * never grant them.\n *\n * Used by every session read on the request path: the main middleware (the\n * first read on a session-bearing request), the auth middleware, and the\n * preview-snapshot route (which bypasses the auth middleware).\n */\nexport async function resolveSessionUser<T>(\n\tsession: { get(key: \"user\"): Promise<T> } | undefined,\n\ttimeoutMs = SESSION_GET_TIMEOUT_MS,\n): Promise<T | undefined> {\n\tif (!session) return undefined;\n\tconst read: Promise<T | undefined> = Promise.resolve(session.get(\"user\")).catch(() => undefined);\n\t// Keep the worker alive past response/cancellation so the read completes and\n\t// the shared promise settles — this is what prevents the isolate poisoning.\n\tafter(() =>\n\t\tread.then(\n\t\t\t() => undefined,\n\t\t\t() => undefined,\n\t\t),\n\t);\n\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\tconst timeout = new Promise<undefined>((resolve) => {\n\t\ttimer = setTimeout(resolve, timeoutMs, undefined);\n\t});\n\ttry {\n\t\treturn await Promise.race([read, timeout]);\n\t} finally {\n\t\tclearTimeout(timer);\n\t}\n}\n"],"mappings":";;;;;;;AAMA,MAAa,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BtC,eAAsB,mBACrB,SACA,YAAY,wBACa;AACzB,KAAI,CAAC,QAAS,QAAO;CACrB,MAAM,OAA+B,QAAQ,QAAQ,QAAQ,IAAI,OAAO,CAAC,CAAC,YAAY,OAAU;AAGhG,aACC,KAAK,WACE,cACA,OACN,CACD;CACD,IAAI;CACJ,MAAM,UAAU,IAAI,SAAoB,YAAY;AACnD,UAAQ,WAAW,SAAS,WAAW,OAAU;GAChD;AACF,KAAI;AACH,SAAO,MAAM,QAAQ,KAAK,CAAC,MAAM,QAAQ,CAAC;WACjC;AACT,eAAa,MAAM"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as getSiteSettingsWithDb, s as setSiteSettings } from "./settings-
|
|
1
|
+
import { c as __exportAll } from "./runner-C1T11CfC.mjs";
|
|
2
|
+
import { a as getSiteSettingsWithDb, s as setSiteSettings } from "./settings-xRdoUZr0.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/api/handlers/settings.ts
|
|
5
5
|
var settings_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -48,4 +48,4 @@ async function handleSettingsUpdate(db, storage, input) {
|
|
|
48
48
|
|
|
49
49
|
//#endregion
|
|
50
50
|
export { handleSettingsUpdate as n, settings_exports as r, handleSettingsGet as t };
|
|
51
|
-
//# sourceMappingURL=settings-
|
|
51
|
+
//# sourceMappingURL=settings-DE9xtzqd.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings-
|
|
1
|
+
{"version":3,"file":"settings-DE9xtzqd.mjs","names":[],"sources":["../src/api/handlers/settings.ts"],"sourcesContent":["/**\n * Settings handlers\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport type { Database } from \"../../database/types.js\";\nimport { getSiteSettingsWithDb, setSiteSettings } from \"../../settings/index.js\";\nimport type { SiteSettings } from \"../../settings/types.js\";\nimport type { Storage } from \"../../storage/types.js\";\nimport type { ApiResult } from \"../types.js\";\n\n/**\n * Get all site settings\n */\nexport async function handleSettingsGet(\n\tdb: Kysely<Database>,\n\tstorage: Storage | null,\n): Promise<ApiResult<Partial<SiteSettings>>> {\n\ttry {\n\t\tconst settings = await getSiteSettingsWithDb(db, storage);\n\t\treturn { success: true, data: settings };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"SETTINGS_READ_ERROR\", message: \"Failed to get settings\" },\n\t\t};\n\t}\n}\n\n/**\n * Update site settings\n */\nexport async function handleSettingsUpdate(\n\tdb: Kysely<Database>,\n\tstorage: Storage | null,\n\tinput: Partial<SiteSettings>,\n): Promise<ApiResult<Partial<SiteSettings>>> {\n\ttry {\n\t\tawait setSiteSettings(input, db);\n\t\tconst updatedSettings = await getSiteSettingsWithDb(db, storage);\n\t\treturn { success: true, data: updatedSettings };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"SETTINGS_UPDATE_ERROR\", message: \"Failed to update settings\" },\n\t\t};\n\t}\n}\n"],"mappings":";;;;;;;;;;;AAeA,eAAsB,kBACrB,IACA,SAC4C;AAC5C,KAAI;AAEH,SAAO;GAAE,SAAS;GAAM,MADP,MAAM,sBAAsB,IAAI,QAAQ;GACjB;SACjC;AACP,SAAO;GACN,SAAS;GACT,OAAO;IAAE,MAAM;IAAuB,SAAS;IAA0B;GACzE;;;;;;AAOH,eAAsB,qBACrB,IACA,SACA,OAC4C;AAC5C,KAAI;AACH,QAAM,gBAAgB,OAAO,GAAG;AAEhC,SAAO;GAAE,SAAS;GAAM,MADA,MAAM,sBAAsB,IAAI,QAAQ;GACjB;SACxC;AACP,SAAO;GACN,SAAS;GACT,OAAO;IAAE,MAAM;IAAyB,SAAS;IAA6B;GAC9E"}
|
|
@@ -4,7 +4,7 @@ import { t as MediaRepository } from "./media-CzhAn2--.mjs";
|
|
|
4
4
|
import { t as OptionsRepository } from "./options-CBXIHUoT.mjs";
|
|
5
5
|
import { n as peekRequestCache, r as requestCached } from "./request-cache-BSUptuJR.mjs";
|
|
6
6
|
import { n as invalidateSingleFlightCache, r as singleFlightCached, t as createSingleFlightCache } from "./single-flight-cache-B0eLKS6K.mjs";
|
|
7
|
-
import { s as getDb } from "./loader-
|
|
7
|
+
import { s as getDb } from "./loader-CdG_Ci3H.mjs";
|
|
8
8
|
|
|
9
9
|
//#region src/settings/index.ts
|
|
10
10
|
/** Object-cache namespace for site settings. */
|
|
@@ -242,4 +242,4 @@ async function getPluginSettingsWithDb(pluginId, db) {
|
|
|
242
242
|
|
|
243
243
|
//#endregion
|
|
244
244
|
export { getSiteSettingsWithDb as a, getSiteSettings as i, getPluginSettings as n, invalidateSiteSettingsCache as o, getSiteSetting as r, setSiteSettings as s, getPluginSetting as t };
|
|
245
|
-
//# sourceMappingURL=settings-
|
|
245
|
+
//# sourceMappingURL=settings-xRdoUZr0.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings-B372MV0p.mjs","names":[],"sources":["../src/settings/index.ts"],"sourcesContent":["/**\n * Site Settings API\n *\n * Functions for getting and setting global site configuration.\n * Settings are stored in the options table with 'site:' prefix.\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { after } from \"../after.js\";\nimport { MediaRepository } from \"../database/repositories/media.js\";\nimport { OptionsRepository } from \"../database/repositories/options.js\";\nimport type { Database } from \"../database/types.js\";\nimport { getDb } from \"../loader.js\";\nimport { cachedQuery, invalidateObjectCache } from \"../object-cache/index.js\";\nimport { peekRequestCache, requestCached } from \"../request-cache.js\";\n\n/** Object-cache namespace for site settings. */\nconst SETTINGS_CACHE_NAMESPACE = \"settings\";\nimport type { Storage } from \"../storage/types.js\";\nimport {\n\tcreateSingleFlightCache,\n\ttype SingleFlightCache,\n\tinvalidateSingleFlightCache,\n\tsingleFlightCached,\n} from \"../utils/single-flight-cache.js\";\nimport type { SiteSettings, SiteSettingKey, MediaReference, SeoSettings } from \"./types.js\";\n\n/** Prefix for site settings in the options table */\nconst SETTINGS_PREFIX = \"site:\";\n\n/**\n * Worker-isolate cache for the resolved `site:*` settings.\n *\n * Site settings (title, logo, SEO defaults) change rarely but are read on\n * every public request. Caching across the isolate's lifetime drops the\n * `options WHERE name LIKE 'site:%'` prefix scan from once-per-request to\n * once-per-isolate. Cross-isolate staleness is bounded by isolate lifetime\n * (workerd typically recycles within minutes); acceptable for chrome.\n *\n * Backed by single-flight-cache.ts: concurrent cold reads coalesce onto one\n * query via a reclaimable single-flight lock and the resolved *value* is\n * cached — never a shared in-flight promise, so a cancelled request can't\n * poison the isolate (see that file's header). Stored on globalThis with a\n * Symbol.for key so Vite SSR chunk duplication doesn't produce two\n * independent caches (same pattern as request-context.ts).\n */\nconst SITE_SETTINGS_CACHE_KEY = Symbol.for(\"emdash:site-settings\");\nconst g = globalThis as Record<symbol, unknown>;\nconst settingsCache: SingleFlightCache<Partial<SiteSettings>> =\n\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- globalThis singleton pattern (see request-context.ts)\n\t(g[SITE_SETTINGS_CACHE_KEY] as SingleFlightCache<Partial<SiteSettings>> | undefined) ??\n\t(() => {\n\t\tconst c = createSingleFlightCache<Partial<SiteSettings>>();\n\t\tg[SITE_SETTINGS_CACHE_KEY] = c;\n\t\treturn c;\n\t})();\n\n/**\n * Bump the isolate-wide site-settings cache version, forcing the next\n * `getSiteSettings()` to re-query the database.\n *\n * Called from every `site:*` write path. Other isolates still serve their\n * own cached copy until they expire — staleness bounded by isolate lifetime.\n */\nexport function invalidateSiteSettingsCache(): void {\n\tinvalidateSingleFlightCache(settingsCache);\n\t// Cross-isolate invalidation for the optional distributed object cache.\n\tinvalidateObjectCache(SETTINGS_CACHE_NAMESPACE);\n}\n\n/**\n * Type guard for MediaReference values\n */\nfunction isMediaReference(value: unknown): value is MediaReference {\n\treturn typeof value === \"object\" && value !== null && \"mediaId\" in value;\n}\n\n/**\n * Resolve a media reference to include the full URL plus content metadata.\n *\n * Pulls `mimeType` and intrinsic dimensions from the media row so callers\n * can emit correct head tags (e.g. `<link rel=\"icon\" type=\"image/svg+xml\">`,\n * which Chromium requires when the URL has no `.svg` extension) without\n * a second round-trip to the media table.\n */\nasync function resolveMediaReference(\n\tmediaRef: MediaReference | undefined,\n\tdb: Kysely<Database>,\n\t_storage: Storage | null,\n): Promise<MediaReference | undefined> {\n\tif (!mediaRef?.mediaId) {\n\t\treturn mediaRef;\n\t}\n\n\ttry {\n\t\tconst mediaRepo = new MediaRepository(db);\n\t\tconst media = await mediaRepo.findById(mediaRef.mediaId);\n\n\t\tif (media) {\n\t\t\t// Construct URL using the same pattern as API handlers\n\t\t\treturn {\n\t\t\t\t...mediaRef,\n\t\t\t\turl: `/_emdash/api/media/file/${media.storageKey}`,\n\t\t\t\tcontentType: media.mimeType,\n\t\t\t\t...(media.width !== null ? { width: media.width } : {}),\n\t\t\t\t...(media.height !== null ? { height: media.height } : {}),\n\t\t\t};\n\t\t}\n\t} catch {\n\t\t// If media not found or error, return the reference as-is\n\t}\n\n\treturn mediaRef;\n}\n\n/**\n * Get a single site setting by key\n *\n * Returns `undefined` if the setting has not been configured.\n * For media settings (logo, favicon), the URL is resolved automatically.\n *\n * @param key - The setting key (e.g., \"title\", \"logo\", \"social\")\n * @returns The setting value, or undefined if not set\n *\n * @example\n * ```ts\n * import { getSiteSetting } from \"emdash\";\n *\n * const title = await getSiteSetting(\"title\");\n * const logo = await getSiteSetting(\"logo\");\n * console.log(logo?.url); // Resolved URL\n * ```\n */\nexport async function getSiteSetting<K extends SiteSettingKey>(\n\tkey: K,\n): Promise<SiteSettings[K] | undefined> {\n\t// If `getSiteSettings()` has already been called in this request,\n\t// read from that (request-cached) batch rather than firing a second\n\t// options-table query. Common layout: a Base template pulls the\n\t// whole settings object up-front, then `EmDashHead` or a plugin\n\t// asks for one key — no reason the singular call should round-trip\n\t// again.\n\tconst primed = peekRequestCache<Partial<SiteSettings>>(\"siteSettings\");\n\tif (primed) {\n\t\tconst settings = await primed;\n\t\treturn settings[key];\n\t}\n\n\t// Otherwise cache per-key. Templates that pull several settings\n\t// independently still share the in-flight query for each one.\n\treturn requestCached(`siteSetting:${key}`, async () => {\n\t\tconst db = await getDb();\n\t\treturn getSiteSettingWithDb(key, db);\n\t});\n}\n\n/**\n * Get a single site setting by key (with explicit db)\n *\n * @internal Use `getSiteSetting()` in templates. This variant is for admin routes\n * that already have a database handle.\n */\nexport async function getSiteSettingWithDb<K extends SiteSettingKey>(\n\tkey: K,\n\tdb: Kysely<Database>,\n\tstorage: Storage | null = null,\n): Promise<SiteSettings[K] | undefined> {\n\tconst options = new OptionsRepository(db);\n\tconst value = await options.get<SiteSettings[K]>(`${SETTINGS_PREFIX}${key}`);\n\n\tif (!value) {\n\t\treturn undefined;\n\t}\n\n\t// Resolve media references if needed.\n\t// TS cannot narrow generic K from key equality checks — this is a known limitation.\n\t// We use the non-generic getSiteSettingsWithDb for media resolution instead.\n\tif ((key === \"logo\" || key === \"favicon\") && isMediaReference(value)) {\n\t\tconst resolved = await resolveMediaReference(value, db, storage);\n\t\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- TS can't narrow generic K from key equality; resolved type is correct\n\t\treturn resolved as SiteSettings[K] | undefined;\n\t}\n\n\tif (key === \"seo\" && value && typeof value === \"object\") {\n\t\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- TS can't narrow generic K from key equality\n\t\tconst seo = value as SeoSettings;\n\t\tif (seo.defaultOgImage) {\n\t\t\tconst resolved = {\n\t\t\t\t...seo,\n\t\t\t\tdefaultOgImage: await resolveMediaReference(seo.defaultOgImage, db, storage),\n\t\t\t};\n\t\t\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- TS can't narrow generic K from key equality\n\t\t\treturn resolved as SiteSettings[K] | undefined;\n\t\t}\n\t}\n\n\treturn value;\n}\n\n/**\n * Get all site settings\n *\n * Returns all configured settings. Unset values are undefined.\n * Media references (logo/favicon) are resolved to include URLs.\n *\n * @example\n * ```ts\n * import { getSiteSettings } from \"emdash\";\n *\n * const settings = await getSiteSettings();\n * console.log(settings.title); // \"My Site\"\n * console.log(settings.logo?.url); // \"/_emdash/api/media/file/abc123\"\n * ```\n */\nexport function getSiteSettings(): Promise<Partial<SiteSettings>> {\n\t// requestCached dedupes within a single request; singleFlightCached\n\t// coalesces across requests and caches the resolved value for the\n\t// global scope's lifetime without ever sharing an awaitable promise. The\n\t// distributed object cache (cachedQuery) sits beneath both, backing cold\n\t// isolates without a database round-trip.\n\treturn requestCached(\"siteSettings\", () =>\n\t\tsingleFlightCached(\n\t\t\tsettingsCache,\n\t\t\t() =>\n\t\t\t\tcachedQuery({\n\t\t\t\t\tnamespace: SETTINGS_CACHE_NAMESPACE,\n\t\t\t\t\tkey: \"all\",\n\t\t\t\t\tload: async () => {\n\t\t\t\t\t\tconst db = await getDb();\n\t\t\t\t\t\treturn getSiteSettingsWithDb(db);\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t{ anchor: (promise) => after(() => promise), ownerTimeoutMs: 30_000 },\n\t\t),\n\t);\n}\n\n/**\n * Get all site settings (with explicit db)\n *\n * @internal Use `getSiteSettings()` in templates. This variant is for admin routes\n * that already have a database handle.\n */\nexport async function getSiteSettingsWithDb(\n\tdb: Kysely<Database>,\n\tstorage: Storage | null = null,\n): Promise<Partial<SiteSettings>> {\n\tconst options = new OptionsRepository(db);\n\tconst allOptions = await options.getByPrefix(SETTINGS_PREFIX);\n\n\tconst settings: Record<string, unknown> = {};\n\n\t// Convert Map to settings object, removing the prefix\n\tfor (const [key, value] of allOptions) {\n\t\tconst settingKey = key.replace(SETTINGS_PREFIX, \"\");\n\t\tsettings[settingKey] = value;\n\t}\n\n\tconst typedSettings = settings as Partial<SiteSettings>;\n\n\t// Resolve media references\n\tif (typedSettings.logo) {\n\t\ttypedSettings.logo = await resolveMediaReference(typedSettings.logo, db, storage);\n\t}\n\tif (typedSettings.favicon) {\n\t\ttypedSettings.favicon = await resolveMediaReference(typedSettings.favicon, db, storage);\n\t}\n\tif (typedSettings.seo?.defaultOgImage) {\n\t\ttypedSettings.seo = {\n\t\t\t...typedSettings.seo,\n\t\t\tdefaultOgImage: await resolveMediaReference(typedSettings.seo.defaultOgImage, db, storage),\n\t\t};\n\t}\n\n\treturn typedSettings;\n}\n\n/**\n * Set site settings (internal function used by admin API)\n *\n * Merges provided settings with existing ones. Only provided fields are updated.\n * Media references should include just the mediaId; URLs are resolved on read.\n *\n * @param settings - Partial settings object with values to update\n * @param db - Kysely database instance\n * @returns Promise that resolves when settings are saved\n *\n * @internal\n *\n * @example\n * ```ts\n * // Update multiple settings at once\n * await setSiteSettings({\n * title: \"My Site\",\n * tagline: \"Welcome\",\n * logo: { mediaId: \"med_123\", alt: \"Logo\" }\n * }, db);\n * ```\n */\nexport async function setSiteSettings(\n\tsettings: Partial<SiteSettings>,\n\tdb: Kysely<Database>,\n): Promise<void> {\n\tconst options = new OptionsRepository(db);\n\n\t// Convert settings to options format\n\tconst updates: Record<string, unknown> = {};\n\tfor (const [key, value] of Object.entries(settings)) {\n\t\tif (value !== undefined) {\n\t\t\tupdates[`${SETTINGS_PREFIX}${key}`] = value;\n\t\t}\n\t}\n\n\ttry {\n\t\tawait options.setMany(updates);\n\t} finally {\n\t\tinvalidateSiteSettingsCache();\n\t}\n}\n\n/**\n * Get a single plugin setting by key.\n *\n * Plugin settings are stored in the options table under\n * `plugin:<pluginId>:settings:<key>`.\n */\nexport async function getPluginSetting<T = unknown>(\n\tpluginId: string,\n\tkey: string,\n): Promise<T | undefined> {\n\tconst db = await getDb();\n\treturn getPluginSettingWithDb<T>(pluginId, key, db);\n}\n\n/**\n * Get a single plugin setting by key (with explicit db).\n *\n * @internal Use `getPluginSetting()` in templates and plugin rendering code.\n */\nexport async function getPluginSettingWithDb<T = unknown>(\n\tpluginId: string,\n\tkey: string,\n\tdb: Kysely<Database>,\n): Promise<T | undefined> {\n\tconst options = new OptionsRepository(db);\n\tconst value = await options.get<T>(`plugin:${pluginId}:settings:${key}`);\n\treturn value ?? undefined;\n}\n\n/**\n * Get all persisted plugin settings for a plugin.\n *\n * Defaults declared in `admin.settingsSchema` are not materialized\n * automatically; callers should apply their own fallback defaults.\n */\nexport async function getPluginSettings(pluginId: string): Promise<Record<string, unknown>> {\n\tconst db = await getDb();\n\treturn getPluginSettingsWithDb(pluginId, db);\n}\n\n/**\n * Get all persisted plugin settings for a plugin (with explicit db).\n *\n * @internal Use `getPluginSettings()` in templates and plugin rendering code.\n */\nexport async function getPluginSettingsWithDb(\n\tpluginId: string,\n\tdb: Kysely<Database>,\n): Promise<Record<string, unknown>> {\n\tconst prefix = `plugin:${pluginId}:settings:`;\n\tconst options = new OptionsRepository(db);\n\tconst allOptions = await options.getByPrefix(prefix);\n\n\tconst settings: Record<string, unknown> = {};\n\tfor (const [key, value] of allOptions) {\n\t\tif (!key.startsWith(prefix)) {\n\t\t\tcontinue;\n\t\t}\n\t\tsettings[key.slice(prefix.length)] = value;\n\t}\n\n\treturn settings;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"settings-xRdoUZr0.mjs","names":[],"sources":["../src/settings/index.ts"],"sourcesContent":["/**\n * Site Settings API\n *\n * Functions for getting and setting global site configuration.\n * Settings are stored in the options table with 'site:' prefix.\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { after } from \"../after.js\";\nimport { MediaRepository } from \"../database/repositories/media.js\";\nimport { OptionsRepository } from \"../database/repositories/options.js\";\nimport type { Database } from \"../database/types.js\";\nimport { getDb } from \"../loader.js\";\nimport { cachedQuery, invalidateObjectCache } from \"../object-cache/index.js\";\nimport { peekRequestCache, requestCached } from \"../request-cache.js\";\n\n/** Object-cache namespace for site settings. */\nconst SETTINGS_CACHE_NAMESPACE = \"settings\";\nimport type { Storage } from \"../storage/types.js\";\nimport {\n\tcreateSingleFlightCache,\n\ttype SingleFlightCache,\n\tinvalidateSingleFlightCache,\n\tsingleFlightCached,\n} from \"../utils/single-flight-cache.js\";\nimport type { SiteSettings, SiteSettingKey, MediaReference, SeoSettings } from \"./types.js\";\n\n/** Prefix for site settings in the options table */\nconst SETTINGS_PREFIX = \"site:\";\n\n/**\n * Worker-isolate cache for the resolved `site:*` settings.\n *\n * Site settings (title, logo, SEO defaults) change rarely but are read on\n * every public request. Caching across the isolate's lifetime drops the\n * `options WHERE name LIKE 'site:%'` prefix scan from once-per-request to\n * once-per-isolate. Cross-isolate staleness is bounded by isolate lifetime\n * (workerd typically recycles within minutes); acceptable for chrome.\n *\n * Backed by single-flight-cache.ts: concurrent cold reads coalesce onto one\n * query via a reclaimable single-flight lock and the resolved *value* is\n * cached — never a shared in-flight promise, so a cancelled request can't\n * poison the isolate (see that file's header). Stored on globalThis with a\n * Symbol.for key so Vite SSR chunk duplication doesn't produce two\n * independent caches (same pattern as request-context.ts).\n */\nconst SITE_SETTINGS_CACHE_KEY = Symbol.for(\"emdash:site-settings\");\nconst g = globalThis as Record<symbol, unknown>;\nconst settingsCache: SingleFlightCache<Partial<SiteSettings>> =\n\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- globalThis singleton pattern (see request-context.ts)\n\t(g[SITE_SETTINGS_CACHE_KEY] as SingleFlightCache<Partial<SiteSettings>> | undefined) ??\n\t(() => {\n\t\tconst c = createSingleFlightCache<Partial<SiteSettings>>();\n\t\tg[SITE_SETTINGS_CACHE_KEY] = c;\n\t\treturn c;\n\t})();\n\n/**\n * Bump the isolate-wide site-settings cache version, forcing the next\n * `getSiteSettings()` to re-query the database.\n *\n * Called from every `site:*` write path. Other isolates still serve their\n * own cached copy until they expire — staleness bounded by isolate lifetime.\n */\nexport function invalidateSiteSettingsCache(): void {\n\tinvalidateSingleFlightCache(settingsCache);\n\t// Cross-isolate invalidation for the optional distributed object cache.\n\tinvalidateObjectCache(SETTINGS_CACHE_NAMESPACE);\n}\n\n/**\n * Type guard for MediaReference values\n */\nfunction isMediaReference(value: unknown): value is MediaReference {\n\treturn typeof value === \"object\" && value !== null && \"mediaId\" in value;\n}\n\n/**\n * Resolve a media reference to include the full URL plus content metadata.\n *\n * Pulls `mimeType` and intrinsic dimensions from the media row so callers\n * can emit correct head tags (e.g. `<link rel=\"icon\" type=\"image/svg+xml\">`,\n * which Chromium requires when the URL has no `.svg` extension) without\n * a second round-trip to the media table.\n */\nasync function resolveMediaReference(\n\tmediaRef: MediaReference | undefined,\n\tdb: Kysely<Database>,\n\t_storage: Storage | null,\n): Promise<MediaReference | undefined> {\n\tif (!mediaRef?.mediaId) {\n\t\treturn mediaRef;\n\t}\n\n\ttry {\n\t\tconst mediaRepo = new MediaRepository(db);\n\t\tconst media = await mediaRepo.findById(mediaRef.mediaId);\n\n\t\tif (media) {\n\t\t\t// Construct URL using the same pattern as API handlers\n\t\t\treturn {\n\t\t\t\t...mediaRef,\n\t\t\t\turl: `/_emdash/api/media/file/${media.storageKey}`,\n\t\t\t\tcontentType: media.mimeType,\n\t\t\t\t...(media.width !== null ? { width: media.width } : {}),\n\t\t\t\t...(media.height !== null ? { height: media.height } : {}),\n\t\t\t};\n\t\t}\n\t} catch {\n\t\t// If media not found or error, return the reference as-is\n\t}\n\n\treturn mediaRef;\n}\n\n/**\n * Get a single site setting by key\n *\n * Returns `undefined` if the setting has not been configured.\n * For media settings (logo, favicon), the URL is resolved automatically.\n *\n * @param key - The setting key (e.g., \"title\", \"logo\", \"social\")\n * @returns The setting value, or undefined if not set\n *\n * @example\n * ```ts\n * import { getSiteSetting } from \"emdash\";\n *\n * const title = await getSiteSetting(\"title\");\n * const logo = await getSiteSetting(\"logo\");\n * console.log(logo?.url); // Resolved URL\n * ```\n */\nexport async function getSiteSetting<K extends SiteSettingKey>(\n\tkey: K,\n): Promise<SiteSettings[K] | undefined> {\n\t// If `getSiteSettings()` has already been called in this request,\n\t// read from that (request-cached) batch rather than firing a second\n\t// options-table query. Common layout: a Base template pulls the\n\t// whole settings object up-front, then `EmDashHead` or a plugin\n\t// asks for one key — no reason the singular call should round-trip\n\t// again.\n\tconst primed = peekRequestCache<Partial<SiteSettings>>(\"siteSettings\");\n\tif (primed) {\n\t\tconst settings = await primed;\n\t\treturn settings[key];\n\t}\n\n\t// Otherwise cache per-key. Templates that pull several settings\n\t// independently still share the in-flight query for each one.\n\treturn requestCached(`siteSetting:${key}`, async () => {\n\t\tconst db = await getDb();\n\t\treturn getSiteSettingWithDb(key, db);\n\t});\n}\n\n/**\n * Get a single site setting by key (with explicit db)\n *\n * @internal Use `getSiteSetting()` in templates. This variant is for admin routes\n * that already have a database handle.\n */\nexport async function getSiteSettingWithDb<K extends SiteSettingKey>(\n\tkey: K,\n\tdb: Kysely<Database>,\n\tstorage: Storage | null = null,\n): Promise<SiteSettings[K] | undefined> {\n\tconst options = new OptionsRepository(db);\n\tconst value = await options.get<SiteSettings[K]>(`${SETTINGS_PREFIX}${key}`);\n\n\tif (!value) {\n\t\treturn undefined;\n\t}\n\n\t// Resolve media references if needed.\n\t// TS cannot narrow generic K from key equality checks — this is a known limitation.\n\t// We use the non-generic getSiteSettingsWithDb for media resolution instead.\n\tif ((key === \"logo\" || key === \"favicon\") && isMediaReference(value)) {\n\t\tconst resolved = await resolveMediaReference(value, db, storage);\n\t\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- TS can't narrow generic K from key equality; resolved type is correct\n\t\treturn resolved as SiteSettings[K] | undefined;\n\t}\n\n\tif (key === \"seo\" && value && typeof value === \"object\") {\n\t\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- TS can't narrow generic K from key equality\n\t\tconst seo = value as SeoSettings;\n\t\tif (seo.defaultOgImage) {\n\t\t\tconst resolved = {\n\t\t\t\t...seo,\n\t\t\t\tdefaultOgImage: await resolveMediaReference(seo.defaultOgImage, db, storage),\n\t\t\t};\n\t\t\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- TS can't narrow generic K from key equality\n\t\t\treturn resolved as SiteSettings[K] | undefined;\n\t\t}\n\t}\n\n\treturn value;\n}\n\n/**\n * Get all site settings\n *\n * Returns all configured settings. Unset values are undefined.\n * Media references (logo/favicon) are resolved to include URLs.\n *\n * @example\n * ```ts\n * import { getSiteSettings } from \"emdash\";\n *\n * const settings = await getSiteSettings();\n * console.log(settings.title); // \"My Site\"\n * console.log(settings.logo?.url); // \"/_emdash/api/media/file/abc123\"\n * ```\n */\nexport function getSiteSettings(): Promise<Partial<SiteSettings>> {\n\t// requestCached dedupes within a single request; singleFlightCached\n\t// coalesces across requests and caches the resolved value for the\n\t// global scope's lifetime without ever sharing an awaitable promise. The\n\t// distributed object cache (cachedQuery) sits beneath both, backing cold\n\t// isolates without a database round-trip.\n\treturn requestCached(\"siteSettings\", () =>\n\t\tsingleFlightCached(\n\t\t\tsettingsCache,\n\t\t\t() =>\n\t\t\t\tcachedQuery({\n\t\t\t\t\tnamespace: SETTINGS_CACHE_NAMESPACE,\n\t\t\t\t\tkey: \"all\",\n\t\t\t\t\tload: async () => {\n\t\t\t\t\t\tconst db = await getDb();\n\t\t\t\t\t\treturn getSiteSettingsWithDb(db);\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t{ anchor: (promise) => after(() => promise), ownerTimeoutMs: 30_000 },\n\t\t),\n\t);\n}\n\n/**\n * Get all site settings (with explicit db)\n *\n * @internal Use `getSiteSettings()` in templates. This variant is for admin routes\n * that already have a database handle.\n */\nexport async function getSiteSettingsWithDb(\n\tdb: Kysely<Database>,\n\tstorage: Storage | null = null,\n): Promise<Partial<SiteSettings>> {\n\tconst options = new OptionsRepository(db);\n\tconst allOptions = await options.getByPrefix(SETTINGS_PREFIX);\n\n\tconst settings: Record<string, unknown> = {};\n\n\t// Convert Map to settings object, removing the prefix\n\tfor (const [key, value] of allOptions) {\n\t\tconst settingKey = key.replace(SETTINGS_PREFIX, \"\");\n\t\tsettings[settingKey] = value;\n\t}\n\n\tconst typedSettings = settings as Partial<SiteSettings>;\n\n\t// Resolve media references\n\tif (typedSettings.logo) {\n\t\ttypedSettings.logo = await resolveMediaReference(typedSettings.logo, db, storage);\n\t}\n\tif (typedSettings.favicon) {\n\t\ttypedSettings.favicon = await resolveMediaReference(typedSettings.favicon, db, storage);\n\t}\n\tif (typedSettings.seo?.defaultOgImage) {\n\t\ttypedSettings.seo = {\n\t\t\t...typedSettings.seo,\n\t\t\tdefaultOgImage: await resolveMediaReference(typedSettings.seo.defaultOgImage, db, storage),\n\t\t};\n\t}\n\n\treturn typedSettings;\n}\n\n/**\n * Set site settings (internal function used by admin API)\n *\n * Merges provided settings with existing ones. Only provided fields are updated.\n * Media references should include just the mediaId; URLs are resolved on read.\n *\n * @param settings - Partial settings object with values to update\n * @param db - Kysely database instance\n * @returns Promise that resolves when settings are saved\n *\n * @internal\n *\n * @example\n * ```ts\n * // Update multiple settings at once\n * await setSiteSettings({\n * title: \"My Site\",\n * tagline: \"Welcome\",\n * logo: { mediaId: \"med_123\", alt: \"Logo\" }\n * }, db);\n * ```\n */\nexport async function setSiteSettings(\n\tsettings: Partial<SiteSettings>,\n\tdb: Kysely<Database>,\n): Promise<void> {\n\tconst options = new OptionsRepository(db);\n\n\t// Convert settings to options format\n\tconst updates: Record<string, unknown> = {};\n\tfor (const [key, value] of Object.entries(settings)) {\n\t\tif (value !== undefined) {\n\t\t\tupdates[`${SETTINGS_PREFIX}${key}`] = value;\n\t\t}\n\t}\n\n\ttry {\n\t\tawait options.setMany(updates);\n\t} finally {\n\t\tinvalidateSiteSettingsCache();\n\t}\n}\n\n/**\n * Get a single plugin setting by key.\n *\n * Plugin settings are stored in the options table under\n * `plugin:<pluginId>:settings:<key>`.\n */\nexport async function getPluginSetting<T = unknown>(\n\tpluginId: string,\n\tkey: string,\n): Promise<T | undefined> {\n\tconst db = await getDb();\n\treturn getPluginSettingWithDb<T>(pluginId, key, db);\n}\n\n/**\n * Get a single plugin setting by key (with explicit db).\n *\n * @internal Use `getPluginSetting()` in templates and plugin rendering code.\n */\nexport async function getPluginSettingWithDb<T = unknown>(\n\tpluginId: string,\n\tkey: string,\n\tdb: Kysely<Database>,\n): Promise<T | undefined> {\n\tconst options = new OptionsRepository(db);\n\tconst value = await options.get<T>(`plugin:${pluginId}:settings:${key}`);\n\treturn value ?? undefined;\n}\n\n/**\n * Get all persisted plugin settings for a plugin.\n *\n * Defaults declared in `admin.settingsSchema` are not materialized\n * automatically; callers should apply their own fallback defaults.\n */\nexport async function getPluginSettings(pluginId: string): Promise<Record<string, unknown>> {\n\tconst db = await getDb();\n\treturn getPluginSettingsWithDb(pluginId, db);\n}\n\n/**\n * Get all persisted plugin settings for a plugin (with explicit db).\n *\n * @internal Use `getPluginSettings()` in templates and plugin rendering code.\n */\nexport async function getPluginSettingsWithDb(\n\tpluginId: string,\n\tdb: Kysely<Database>,\n): Promise<Record<string, unknown>> {\n\tconst prefix = `plugin:${pluginId}:settings:`;\n\tconst options = new OptionsRepository(db);\n\tconst allOptions = await options.getByPrefix(prefix);\n\n\tconst settings: Record<string, unknown> = {};\n\tfor (const [key, value] of allOptions) {\n\t\tif (!key.startsWith(prefix)) {\n\t\t\tcontinue;\n\t\t}\n\t\tsettings[key.slice(prefix.length)] = value;\n\t}\n\n\treturn settings;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setup-complete-
|
|
1
|
+
{"version":3,"file":"setup-complete-BnT8pq_C.mjs","names":[],"sources":["../src/api/setup-complete.ts"],"sourcesContent":["/**\n * Shared setup completion logic.\n *\n * Called by OAuth callbacks and the passkey verify step when the first user\n * is created during setup. Persists site title/tagline from setup state\n * and marks setup as complete.\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { OptionsRepository } from \"../database/repositories/options.js\";\nimport type { Database } from \"../database/types.js\";\n\n/**\n * Finalize setup after the first admin user is created.\n *\n * Reads the setup_state option (written by the setup wizard's step 1),\n * persists site_title and site_tagline, then marks setup complete.\n *\n * Safe to call multiple times — checks setup_complete first and no-ops\n * if already done.\n */\nexport async function finalizeSetup(db: Kysely<Database>): Promise<void> {\n\tconst options = new OptionsRepository(db);\n\n\tconst setupComplete = await options.get(\"emdash:setup_complete\");\n\tif (setupComplete === true || setupComplete === \"true\") return;\n\n\t// Persist site title/tagline from setup state (stored in step 1)\n\tconst setupState = await options.get<Record<string, unknown>>(\"emdash:setup_state\");\n\tif (setupState?.title && typeof setupState.title === \"string\") {\n\t\tawait options.set(\"emdash:site_title\", setupState.title);\n\t}\n\tif (setupState?.tagline && typeof setupState.tagline === \"string\") {\n\t\tawait options.set(\"emdash:site_tagline\", setupState.tagline);\n\t}\n\n\tawait options.set(\"emdash:setup_complete\", true);\n\tawait options.delete(\"emdash:setup_state\");\n}\n"],"mappings":";;;;;;;;;;;;AAsBA,eAAsB,cAAc,IAAqC;CACxE,MAAM,UAAU,IAAI,kBAAkB,GAAG;CAEzC,MAAM,gBAAgB,MAAM,QAAQ,IAAI,wBAAwB;AAChE,KAAI,kBAAkB,QAAQ,kBAAkB,OAAQ;CAGxD,MAAM,aAAa,MAAM,QAAQ,IAA6B,qBAAqB;AACnF,KAAI,YAAY,SAAS,OAAO,WAAW,UAAU,SACpD,OAAM,QAAQ,IAAI,qBAAqB,WAAW,MAAM;AAEzD,KAAI,YAAY,WAAW,OAAO,WAAW,YAAY,SACxD,OAAM,QAAQ,IAAI,uBAAuB,WAAW,QAAQ;AAG7D,OAAM,QAAQ,IAAI,yBAAyB,KAAK;AAChD,OAAM,QAAQ,OAAO,qBAAqB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setup-nonce-
|
|
1
|
+
{"version":3,"file":"setup-nonce-CBitf_oQ.mjs","names":[],"sources":["../src/auth/setup-nonce.ts"],"sourcesContent":["/**\n * Session binding for the first-setup admin-creation flow.\n *\n * Shared constants for the nonce cookie that ties /_emdash/api/setup/admin\n * and /_emdash/api/setup/admin/verify to the same browser. Without this\n * binding, any unauthenticated caller could POST /setup/admin during the\n * setup window and substitute their own email into the stored setup state\n * before the legitimate admin completes passkey verification.\n *\n * Implementation lives in the two route handlers; this module is just\n * the name / lifetime so both ends agree.\n */\n\n/** Cookie name carrying the setup-admin session nonce. */\nexport const SETUP_NONCE_COOKIE = \"emdash_setup_nonce\";\n\n/**\n * Cookie max-age in seconds. One hour is plenty of time to complete\n * a passkey registration; if the user lingers longer the admin step\n * can simply be retried.\n */\nexport const SETUP_NONCE_MAX_AGE_SECONDS = 60 * 60;\n"],"mappings":";;;;;;;;;;;;;;AAcA,MAAa,qBAAqB;;;;;;AAOlC,MAAa,8BAA8B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"site-url-
|
|
1
|
+
{"version":3,"file":"site-url-DjpIXMXJ.mjs","names":[],"sources":["../src/api/site-url.ts"],"sourcesContent":["/**\n * Resolve the canonical site base URL for use in outbound links (emails, etc.).\n *\n * Precedence mirrors `getPublicOrigin`: the operator-configured origin\n * (`config.siteUrl`, then the `EMDASH_SITE_URL`/`SITE_URL` env vars) wins,\n * then the stored `emdash:site_url` option (written once during setup on the\n * real domain), and only before setup completes does the request URL fill in.\n * A configured or stored value always beats the request, so Host header\n * spoofing cannot redirect users to attacker-controlled domains.\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { OptionsRepository } from \"../database/repositories/options.js\";\nimport type { Database } from \"../database/types.js\";\nimport { getConfiguredOrigin, type SiteUrlConfig } from \"./public-url.js\";\n\nexport async function getSiteBaseUrl(\n\tdb: Kysely<Database>,\n\trequest: Request,\n\tconfig?: SiteUrlConfig,\n): Promise<string> {\n\tconst configured = getConfiguredOrigin(config);\n\tif (configured) {\n\t\treturn `${configured}/_emdash`;\n\t}\n\tconst options = new OptionsRepository(db);\n\tconst storedUrl = await options.get<string>(\"emdash:site_url\");\n\tif (storedUrl) {\n\t\treturn `${storedUrl}/_emdash`;\n\t}\n\t// Fallback: derive from request (only reached before setup completes)\n\tconst url = new URL(request.url);\n\treturn `${url.protocol}//${url.host}/_emdash`;\n}\n"],"mappings":";;;;AAiBA,eAAsB,eACrB,IACA,SACA,QACkB;CAClB,MAAM,aAAa,oBAAoB,OAAO;AAC9C,KAAI,WACH,QAAO,GAAG,WAAW;CAGtB,MAAM,YAAY,MADF,IAAI,kBAAkB,GAAG,CACT,IAAY,kBAAkB;AAC9D,KAAI,UACH,QAAO,GAAG,UAAU;CAGrB,MAAM,MAAM,IAAI,IAAI,QAAQ,IAAI;AAChC,QAAO,GAAG,IAAI,SAAS,IAAI,IAAI,KAAK"}
|
|
@@ -213,4 +213,4 @@ async function generateSnapshot(db, options) {
|
|
|
213
213
|
|
|
214
214
|
//#endregion
|
|
215
215
|
export { parsePreviewSignatureHeader as n, verifyPreviewSignature as r, generateSnapshot as t };
|
|
216
|
-
//# sourceMappingURL=snapshot-
|
|
216
|
+
//# sourceMappingURL=snapshot-BBlGkb2q.mjs.map
|