emdash 0.19.0 → 0.20.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-C5AWLJSD.d.mts → adapters-BzIHV3sw.d.mts} +1 -1
- package/dist/{adapters-C5AWLJSD.d.mts.map → adapters-BzIHV3sw.d.mts.map} +1 -1
- package/dist/{allowed-origins-CyYLEJkp.mjs → allowed-origins-B1u7Qnvg.mjs} +2 -2
- package/dist/{allowed-origins-CyYLEJkp.mjs.map → allowed-origins-B1u7Qnvg.mjs.map} +1 -1
- package/dist/api/route-utils.d.mts +3 -3
- package/dist/api/route-utils.mjs +5 -5
- package/dist/api/schemas/index.d.mts +1 -1
- package/dist/api/schemas/index.mjs +2 -2
- package/dist/{api-BZ6bhjYs.mjs → api-DStv36ik.mjs} +36 -5
- package/dist/api-DStv36ik.mjs.map +1 -0
- package/dist/{api-tokens-VrXNiNvV.mjs → api-tokens-DPfhPu5V.mjs} +2 -2
- package/dist/{api-tokens-VrXNiNvV.mjs.map → api-tokens-DPfhPu5V.mjs.map} +1 -1
- package/dist/{apply-hQkKKBCf.mjs → apply-Dr7snAMT.mjs} +7 -7
- package/dist/{apply-hQkKKBCf.mjs.map → apply-Dr7snAMT.mjs.map} +1 -1
- package/dist/astro/index.d.mts +10 -10
- package/dist/astro/index.mjs +3 -3
- package/dist/astro/middleware/auth.d.mts +9 -9
- package/dist/astro/middleware/auth.mjs +4 -4
- package/dist/astro/middleware/redirect.mjs +1 -1
- package/dist/astro/middleware/request-context.mjs +1 -1
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.d.mts +1 -1
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +63 -112
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +2 -2
- package/dist/astro/routes/api/admin/allowed-domains/index.mjs +2 -2
- 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 +4 -4
- package/dist/astro/routes/api/admin/byline-fields/index.mjs +4 -4
- package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +4 -4
- package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +6 -6
- package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +6 -6
- package/dist/astro/routes/api/admin/bylines/index.mjs +6 -6
- package/dist/astro/routes/api/admin/comments/_id_/status.mjs +6 -6
- package/dist/astro/routes/api/admin/comments/_id_.mjs +2 -2
- package/dist/astro/routes/api/admin/comments/bulk.mjs +4 -4
- package/dist/astro/routes/api/admin/comments/counts.mjs +2 -2
- package/dist/astro/routes/api/admin/comments/index.mjs +4 -4
- 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/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 +14 -14
- package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +14 -14
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +14 -14
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +14 -14
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +14 -14
- package/dist/astro/routes/api/admin/plugins/index.mjs +14 -14
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +14 -14
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +14 -14
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +14 -14
- package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +14 -14
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +15 -15
- package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +14 -14
- package/dist/astro/routes/api/admin/plugins/registry/install.mjs +15 -15
- package/dist/astro/routes/api/admin/plugins/updates.mjs +14 -14
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +14 -14
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +1 -1
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +14 -14
- package/dist/astro/routes/api/admin/users/_id_/index.mjs +2 -2
- package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +1 -1
- package/dist/astro/routes/api/admin/users/index.mjs +2 -2
- package/dist/astro/routes/api/auth/dev-bypass.mjs +2 -2
- package/dist/astro/routes/api/auth/invite/complete.mjs +6 -6
- package/dist/astro/routes/api/auth/invite/index.mjs +3 -3
- package/dist/astro/routes/api/auth/invite/register-options.mjs +5 -5
- package/dist/astro/routes/api/auth/logout.mjs +1 -1
- package/dist/astro/routes/api/auth/magic-link/send.mjs +4 -4
- 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 +3 -3
- package/dist/astro/routes/api/auth/oauth/_provider_.mjs +2 -2
- package/dist/astro/routes/api/auth/passkey/_id_.mjs +2 -2
- package/dist/astro/routes/api/auth/passkey/options.mjs +6 -6
- package/dist/astro/routes/api/auth/passkey/register/options.mjs +5 -5
- package/dist/astro/routes/api/auth/passkey/register/verify.mjs +6 -6
- package/dist/astro/routes/api/auth/passkey/verify.mjs +6 -6
- package/dist/astro/routes/api/auth/signup/complete.mjs +6 -6
- package/dist/astro/routes/api/auth/signup/request.mjs +4 -4
- package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +6 -6
- 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 +4 -4
- package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +3 -3
- 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 +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +5 -5
- 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 +3 -3
- package/dist/astro/routes/api/content/_collection_/authors.mjs +1 -1
- package/dist/astro/routes/api/content/_collection_/index.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/trash.mjs +3 -3
- package/dist/astro/routes/api/dashboard.mjs +1 -1
- package/dist/astro/routes/api/import/probe.d.mts +3 -3
- package/dist/astro/routes/api/import/probe.mjs +3 -3
- package/dist/astro/routes/api/import/wordpress/analyze.mjs +1 -1
- package/dist/astro/routes/api/import/wordpress/execute.d.mts +9 -9
- package/dist/astro/routes/api/import/wordpress/execute.mjs +3 -3
- package/dist/astro/routes/api/import/wordpress/media.mjs +3 -3
- package/dist/astro/routes/api/import/wordpress/prepare.mjs +3 -3
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +3 -3
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.d.mts +1 -1
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +3 -3
- package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +1 -1
- package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +3 -3
- package/dist/astro/routes/api/manifest.mjs +2 -2
- package/dist/astro/routes/api/mcp.mjs +18 -18
- package/dist/astro/routes/api/media/_id_/confirm.mjs +3 -3
- package/dist/astro/routes/api/media/_id_.mjs +3 -3
- 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 +4 -4
- package/dist/astro/routes/api/media.mjs +4 -4
- 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 +6 -6
- package/dist/astro/routes/api/oauth/device/authorize.mjs +3 -3
- package/dist/astro/routes/api/oauth/device/code.mjs +5 -5
- 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 +2 -2
- package/dist/astro/routes/api/redirects/404s/index.mjs +4 -4
- package/dist/astro/routes/api/redirects/404s/summary.mjs +4 -4
- 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/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 +14 -14
- package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +14 -14
- package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +14 -14
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +14 -14
- package/dist/astro/routes/api/schema/collections/index.mjs +14 -14
- package/dist/astro/routes/api/schema/index.mjs +5 -10
- package/dist/astro/routes/api/schema/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs +14 -14
- package/dist/astro/routes/api/schema/orphans/index.mjs +14 -14
- 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 +3 -3
- package/dist/astro/routes/api/search/suggest.mjs +4 -4
- package/dist/astro/routes/api/sections/_slug_.mjs +5 -5
- package/dist/astro/routes/api/sections/index.mjs +5 -5
- package/dist/astro/routes/api/settings/email.mjs +1 -1
- package/dist/astro/routes/api/settings.mjs +6 -6
- package/dist/astro/routes/api/setup/admin-verify.mjs +7 -7
- package/dist/astro/routes/api/setup/admin.mjs +6 -6
- package/dist/astro/routes/api/setup/dev-bypass.mjs +10 -10
- package/dist/astro/routes/api/setup/index.mjs +9 -9
- package/dist/astro/routes/api/setup/status.mjs +2 -2
- package/dist/astro/routes/api/snapshot.mjs +3 -3
- 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/index.mjs +6 -6
- package/dist/astro/routes/api/themes/preview.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 +2 -2
- package/dist/astro/routes/api/well-known/oauth-protected-resource.mjs +2 -2
- package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +3 -3
- package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +6 -5
- package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +6 -5
- package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs.map +1 -1
- package/dist/astro/routes/api/widget-areas/_name_.mjs +4 -3
- package/dist/astro/routes/api/widget-areas/_name_.mjs.map +1 -1
- package/dist/astro/routes/api/widget-areas/index.mjs +6 -5
- package/dist/astro/routes/api/widget-areas/index.mjs.map +1 -1
- package/dist/astro/routes/api/widget-components.mjs +1 -1
- package/dist/astro/routes/robots.txt.mjs +3 -3
- package/dist/astro/routes/sitemap-_collection_.xml.d.mts.map +1 -1
- package/dist/astro/routes/sitemap-_collection_.xml.mjs +12 -5
- package/dist/astro/routes/sitemap-_collection_.xml.mjs.map +1 -1
- package/dist/astro/routes/sitemap.xml.mjs +4 -4
- package/dist/astro/types.d.mts +12 -12
- package/dist/auth/providers/github.d.mts +1 -1
- package/dist/auth/providers/google.d.mts +1 -1
- package/dist/{authorize-C_8t2KGa.mjs → authorize-DsMSVSaY.mjs} +1 -1
- package/dist/{authorize-C_8t2KGa.mjs.map → authorize-DsMSVSaY.mjs.map} +1 -1
- package/dist/{byline-fields-C_OsR-KF.mjs → byline-fields--WxSNS79.mjs} +1 -1
- package/dist/{byline-fields-C_OsR-KF.mjs.map → byline-fields--WxSNS79.mjs.map} +1 -1
- package/dist/{byline-fields-51kg6Vuv.mjs → byline-fields-8TMtkBnH.mjs} +2 -2
- package/dist/{byline-fields-51kg6Vuv.mjs.map → byline-fields-8TMtkBnH.mjs.map} +1 -1
- package/dist/{byline-fields-DYXKDuNX.d.mts → byline-fields-DbibsvTl.d.mts} +5 -1
- package/dist/byline-fields-DbibsvTl.d.mts.map +1 -0
- package/dist/{bylines-Cx5n-WqP.mjs → bylines-BdxWCnPL.mjs} +1 -1
- package/dist/{bylines-Cx5n-WqP.mjs.map → bylines-BdxWCnPL.mjs.map} +1 -1
- package/dist/{bylines-wurS258E.mjs → bylines-s8c2DXbH.mjs} +3 -3
- package/dist/{bylines-wurS258E.mjs.map → bylines-s8c2DXbH.mjs.map} +1 -1
- package/dist/{challenge-store-DGwuCc4R.mjs → challenge-store-DXX3rfdI.mjs} +1 -1
- package/dist/{challenge-store-DGwuCc4R.mjs.map → challenge-store-DXX3rfdI.mjs.map} +1 -1
- package/dist/cli/index.mjs +11 -10
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/cf-access.d.mts +1 -1
- package/dist/client/index.d.mts +1 -1
- package/dist/client/index.mjs +1 -1
- package/dist/{comments-CJ0RZsYR.mjs → comments-Vkivawyl.mjs} +1 -1
- package/dist/{comments-CJ0RZsYR.mjs.map → comments-Vkivawyl.mjs.map} +1 -1
- package/dist/{components-CTfpu3PZ.mjs → components-CK0cuUoH.mjs} +1 -1
- package/dist/{components-CTfpu3PZ.mjs.map → components-CK0cuUoH.mjs.map} +1 -1
- package/dist/{context-GG52SPgh.mjs → context-Y7BRkWes.mjs} +2 -2
- package/dist/{context-GG52SPgh.mjs.map → context-Y7BRkWes.mjs.map} +1 -1
- package/dist/database/instrumentation.d.mts +10 -1
- package/dist/database/instrumentation.d.mts.map +1 -1
- package/dist/database/instrumentation.mjs +13 -1
- package/dist/database/instrumentation.mjs.map +1 -1
- package/dist/db/index.d.mts +3 -3
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/{default-xLFNSsZ9.mjs → default-IlBaTFxM.mjs} +1 -1
- package/dist/{default-xLFNSsZ9.mjs.map → default-IlBaTFxM.mjs.map} +1 -1
- package/dist/{device-flow-s6_q3T7A.mjs → device-flow-R23SIbQ2.mjs} +4 -4
- package/dist/{device-flow-s6_q3T7A.mjs.map → device-flow-R23SIbQ2.mjs.map} +1 -1
- package/dist/{escape-bIyGoW5W.mjs → escape-Ds07EEyu.mjs} +1 -1
- package/dist/{escape-bIyGoW5W.mjs.map → escape-Ds07EEyu.mjs.map} +1 -1
- package/dist/{index-FfiTQJq2.d.mts → index-B1keaX5Y.d.mts} +43 -12
- package/dist/{index-FfiTQJq2.d.mts.map → index-B1keaX5Y.d.mts.map} +1 -1
- package/dist/{index-BpYeJO1E.d.mts → index-DR56od45.d.mts} +3 -3
- package/dist/{index-BpYeJO1E.d.mts.map → index-DR56od45.d.mts.map} +1 -1
- package/dist/index.d.mts +16 -16
- package/dist/index.mjs +22 -22
- package/dist/{load-B84ohfBk.mjs → load-BBetCvLC.mjs} +1 -1
- package/dist/{load-B84ohfBk.mjs.map → load-BBetCvLC.mjs.map} +1 -1
- package/dist/{loader-CpZKpFz0.mjs → loader-ZN1ll-d-.mjs} +11 -14
- package/dist/loader-ZN1ll-d-.mjs.map +1 -0
- package/dist/{manifest-schema-Cj-YrzrF.mjs → manifest-schema-BtwbL_vj.mjs} +55 -2
- package/dist/manifest-schema-BtwbL_vj.mjs.map +1 -0
- package/dist/media/index.d.mts +1 -1
- package/dist/media/local-runtime.d.mts +11 -11
- package/dist/media/local-runtime.mjs +2 -2
- package/dist/{media-allowlist-CMcoYIjQ.mjs → media-allowlist-Dknq-OFY.mjs} +1 -1
- package/dist/{media-allowlist-CMcoYIjQ.mjs.map → media-allowlist-Dknq-OFY.mjs.map} +1 -1
- package/dist/media-url-VClf8glU.mjs +26 -0
- package/dist/media-url-VClf8glU.mjs.map +1 -0
- package/dist/{menus-Dp9xporj.mjs → menus-DrQLusqj.mjs} +6 -33
- package/dist/menus-DrQLusqj.mjs.map +1 -0
- package/dist/{mode-BjlXswIw.mjs → mode-CO2vQHfq.mjs} +1 -1
- package/dist/{mode-BjlXswIw.mjs.map → mode-CO2vQHfq.mjs.map} +1 -1
- package/dist/{oauth-authorization-1aPAYjiC.mjs → oauth-authorization-Bw4NdF_S.mjs} +4 -4
- package/dist/{oauth-authorization-1aPAYjiC.mjs.map → oauth-authorization-Bw4NdF_S.mjs.map} +1 -1
- package/dist/{oauth-clients-8mPDStMv.mjs → oauth-clients-BGGFp57s.mjs} +1 -1
- package/dist/{oauth-clients-8mPDStMv.mjs.map → oauth-clients-BGGFp57s.mjs.map} +1 -1
- package/dist/{oauth-state-store-BJ7YtrfD.mjs → oauth-state-store-97x0xtN2.mjs} +1 -1
- package/dist/{oauth-state-store-BJ7YtrfD.mjs.map → oauth-state-store-97x0xtN2.mjs.map} +1 -1
- package/dist/{oauth-user-lookup-BdDSDvjF.mjs → oauth-user-lookup-B_vnZHKO.mjs} +1 -1
- package/dist/{oauth-user-lookup-BdDSDvjF.mjs.map → oauth-user-lookup-B_vnZHKO.mjs.map} +1 -1
- package/dist/{options-D4MnavW_.d.mts → options-DyYIYpPd.d.mts} +3 -3
- package/dist/{options-D4MnavW_.d.mts.map → options-DyYIYpPd.d.mts.map} +1 -1
- package/dist/page/index.d.mts +2 -2
- package/dist/{passkey-config-BDVM86Tj.mjs → passkey-config-C3QgnQnU.mjs} +1 -1
- package/dist/{passkey-config-BDVM86Tj.mjs.map → passkey-config-C3QgnQnU.mjs.map} +1 -1
- package/dist/{placeholder-B9lUUEmj.d.mts → placeholder-CVBv5z8k.d.mts} +1 -1
- package/dist/{placeholder-B9lUUEmj.d.mts.map → placeholder-CVBv5z8k.d.mts.map} +1 -1
- package/dist/plugin-types.d.mts +1 -1
- package/dist/plugin-utils.d.mts +9 -9
- package/dist/plugins/adapt-sandbox-entry.d.mts +9 -9
- package/dist/plugins/adapt-sandbox-entry.mjs +2 -2
- package/dist/{public-url-egRHCy1m.mjs → public-url-BFVC2OTJ.mjs} +1 -1
- package/dist/{public-url-egRHCy1m.mjs.map → public-url-BFVC2OTJ.mjs.map} +1 -1
- package/dist/{query-BFQ029Ts.mjs → query-CbUcI4Xk.mjs} +18 -8
- package/dist/query-CbUcI4Xk.mjs.map +1 -0
- package/dist/{rate-limit-ClFFUga6.mjs → rate-limit-C7hjdkS5.mjs} +1 -1
- package/dist/{rate-limit-ClFFUga6.mjs.map → rate-limit-C7hjdkS5.mjs.map} +1 -1
- package/dist/{redirect-Cw3JTlmj.mjs → redirect-B_q19j4v.mjs} +1 -1
- package/dist/{redirect-Cw3JTlmj.mjs.map → redirect-B_q19j4v.mjs.map} +1 -1
- package/dist/{redirects-DEygMrRO.mjs → redirects-CCbCqCCd.mjs} +4 -2
- package/dist/redirects-CCbCqCCd.mjs.map +1 -0
- package/dist/{redirects-OIu6vQ2i.mjs → redirects-DxVoR7PI.mjs} +1 -1
- package/dist/{redirects-OIu6vQ2i.mjs.map → redirects-DxVoR7PI.mjs.map} +1 -1
- package/dist/request-context.d.mts +7 -0
- package/dist/request-context.d.mts.map +1 -1
- package/dist/request-context.mjs +2 -1
- package/dist/request-context.mjs.map +1 -1
- package/dist/{runner-BcRuXq_h.d.mts → runner-DTdhuI9i.d.mts} +2 -2
- package/dist/{runner-BcRuXq_h.d.mts.map → runner-DTdhuI9i.d.mts.map} +1 -1
- package/dist/runtime.d.mts +10 -10
- package/dist/runtime.mjs +1 -1
- package/dist/{schema-CS7Eg5gh.mjs → schema-C1E70ug_.mjs} +2 -2
- package/dist/{schema-CS7Eg5gh.mjs.map → schema-C1E70ug_.mjs.map} +1 -1
- package/dist/{search-o-aQzHI1.mjs → search-B3SGZw91.mjs} +2 -2
- package/dist/{search-o-aQzHI1.mjs.map → search-B3SGZw91.mjs.map} +1 -1
- package/dist/{secrets-C_ZtRos3.mjs → secrets-ChPTmy9x.mjs} +1 -1
- package/dist/{secrets-C_ZtRos3.mjs.map → secrets-ChPTmy9x.mjs.map} +1 -1
- package/dist/{sections-DhsZ0ns9.mjs → sections-D_lVzwRZ.mjs} +2 -2
- package/dist/{sections-DhsZ0ns9.mjs.map → sections-D_lVzwRZ.mjs.map} +1 -1
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +6 -6
- package/dist/seo/index.d.mts +1 -1
- package/dist/seo/index.d.mts.map +1 -1
- package/dist/seo/index.mjs +3 -12
- package/dist/seo/index.mjs.map +1 -1
- package/dist/{seo-DfjLvu8i.mjs → seo-D_LPkOtu.mjs} +4 -3
- package/dist/seo-D_LPkOtu.mjs.map +1 -0
- package/dist/{service-DAxg8RPR.mjs → service-ChDcsTBs.mjs} +2 -2
- package/dist/{service-DAxg8RPR.mjs.map → service-ChDcsTBs.mjs.map} +1 -1
- package/dist/{settings-DIsbHTRE.mjs → settings-Cv47v9u8.mjs} +2 -2
- package/dist/{settings-DIsbHTRE.mjs.map → settings-Cv47v9u8.mjs.map} +1 -1
- package/dist/settings-DfxiWY_s.mjs +411 -0
- package/dist/settings-DfxiWY_s.mjs.map +1 -0
- package/dist/{setup-complete-Yuv78yua.mjs → setup-complete-yvPE4OsP.mjs} +1 -1
- package/dist/{setup-complete-Yuv78yua.mjs.map → setup-complete-yvPE4OsP.mjs.map} +1 -1
- package/dist/{setup-nonce-Bm0uKqmf.mjs → setup-nonce-C9aFzb94.mjs} +1 -1
- package/dist/{setup-nonce-Bm0uKqmf.mjs.map → setup-nonce-C9aFzb94.mjs.map} +1 -1
- package/dist/{site-url-mEVmwIFi.mjs → site-url-CnHlmAs9.mjs} +1 -1
- package/dist/{site-url-mEVmwIFi.mjs.map → site-url-CnHlmAs9.mjs.map} +1 -1
- package/dist/storage/local.d.mts +1 -1
- package/dist/storage/s3.d.mts +1 -1
- package/dist/{taxonomies-UusDXv3C.mjs → taxonomies-BILwiyGk.mjs} +2 -2
- package/dist/{taxonomies-UusDXv3C.mjs.map → taxonomies-BILwiyGk.mjs.map} +1 -1
- package/dist/{taxonomies-BEW7S5AI.mjs → taxonomies-BdAmbOwx.mjs} +46 -9
- package/dist/taxonomies-BdAmbOwx.mjs.map +1 -0
- package/dist/{transport-BwQeeY2p.d.mts → transport-B7PPP2CC.d.mts} +1 -1
- package/dist/{transport-BwQeeY2p.d.mts.map → transport-B7PPP2CC.d.mts.map} +1 -1
- package/dist/{transport--Ck3RBin.mjs → transport-CmpLD7W3.mjs} +1 -1
- package/dist/{transport--Ck3RBin.mjs.map → transport-CmpLD7W3.mjs.map} +1 -1
- package/dist/{types-DWnN7weG.d.mts → types-BFgrqwSk.d.mts} +1 -1
- package/dist/{types-DWnN7weG.d.mts.map → types-BFgrqwSk.d.mts.map} +1 -1
- package/dist/{types-Qa7-HJJC.d.mts → types-BH8-30hc.d.mts} +1 -1
- package/dist/{types-Qa7-HJJC.d.mts.map → types-BH8-30hc.d.mts.map} +1 -1
- package/dist/{types-OT_Es5mp.d.mts → types-BPzXTV9x.d.mts} +1 -1
- package/dist/{types-OT_Es5mp.d.mts.map → types-BPzXTV9x.d.mts.map} +1 -1
- package/dist/{types-DbCWhHet.d.mts → types-BUUVn1zr.d.mts} +2 -2
- package/dist/types-BUUVn1zr.d.mts.map +1 -0
- package/dist/{types-DMwSpvcw.d.mts → types-CPAPl93j.d.mts} +9 -3
- package/dist/{types-DMwSpvcw.d.mts.map → types-CPAPl93j.d.mts.map} +1 -1
- package/dist/types-CZI4E3qG.mjs +3 -0
- package/dist/{types-kwqCOUxj.d.mts → types-D4kUqbHh.d.mts} +1 -1
- package/dist/{types-kwqCOUxj.d.mts.map → types-D4kUqbHh.d.mts.map} +1 -1
- package/dist/{types-WVmpZBJV.d.mts → types-DTniiNto.d.mts} +2 -2
- package/dist/{types-WVmpZBJV.d.mts.map → types-DTniiNto.d.mts.map} +1 -1
- package/dist/types-DZk_y-MU.mjs.map +1 -1
- package/dist/{types-DX6v9KzJ.d.mts → types-S15DXXNi.d.mts} +1 -1
- package/dist/{types-DX6v9KzJ.d.mts.map → types-S15DXXNi.d.mts.map} +1 -1
- package/dist/{validate-ZP9Dvg0P.mjs → validate-Bz4vqcX1.mjs} +1 -1
- package/dist/{validate-ZP9Dvg0P.mjs.map → validate-Bz4vqcX1.mjs.map} +1 -1
- package/dist/{validate-BPAHUSge.d.mts → validate-CNwkPWzz.d.mts} +5 -5
- package/dist/{validate-BPAHUSge.d.mts.map → validate-CNwkPWzz.d.mts.map} +1 -1
- package/dist/{validation-CE5i4q0c.mjs → validation-DgGTJm3u.mjs} +1 -1
- package/dist/{validation-CE5i4q0c.mjs.map → validation-DgGTJm3u.mjs.map} +1 -1
- package/dist/version-D-5txk2m.mjs +7 -0
- package/dist/{version-Dw0JXu45.mjs.map → version-D-5txk2m.mjs.map} +1 -1
- package/dist/{widgets-ClEnYQCH.mjs → widgets-DZfmAbE4.mjs} +47 -44
- package/dist/widgets-DZfmAbE4.mjs.map +1 -0
- package/package.json +10 -10
- package/src/api/handlers/marketplace.ts +2 -5
- package/src/api/handlers/registry.ts +70 -0
- package/src/api/handlers/seo.ts +9 -1
- package/src/api/schemas/schema.ts +13 -1
- package/src/astro/middleware.ts +20 -6
- package/src/astro/routes/api/schema/index.ts +7 -15
- package/src/astro/routes/sitemap-[collection].xml.ts +13 -2
- package/src/cli/commands/bundle-utils.ts +2 -0
- package/src/cli/commands/secrets.ts +2 -2
- package/src/database/instrumentation.ts +13 -0
- package/src/emdash-runtime.ts +31 -25
- package/src/loader.ts +24 -15
- package/src/plugins/manifest-schema.ts +75 -0
- package/src/plugins/marketplace.ts +2 -5
- package/src/plugins/types.ts +12 -0
- package/src/query.ts +13 -2
- package/src/request-context.ts +8 -0
- package/src/schema/types.ts +11 -1
- package/src/seo/index.ts +2 -28
- package/src/seo/media-url.ts +32 -0
- package/src/settings/index.ts +32 -40
- package/src/taxonomies/index.ts +78 -12
- package/src/utils/isolate-cache.ts +189 -0
- package/src/widgets/index.ts +57 -54
- package/dist/api-BZ6bhjYs.mjs.map +0 -1
- package/dist/byline-fields-DYXKDuNX.d.mts.map +0 -1
- package/dist/loader-CpZKpFz0.mjs.map +0 -1
- package/dist/manifest-schema-Cj-YrzrF.mjs.map +0 -1
- package/dist/menus-Dp9xporj.mjs.map +0 -1
- package/dist/query-BFQ029Ts.mjs.map +0 -1
- package/dist/redirects-DEygMrRO.mjs.map +0 -1
- package/dist/seo-DfjLvu8i.mjs.map +0 -1
- package/dist/settings-B1p-gPUK.mjs +0 -235
- package/dist/settings-B1p-gPUK.mjs.map +0 -1
- package/dist/taxonomies-BEW7S5AI.mjs.map +0 -1
- package/dist/types-Cj2S6FuC.mjs +0 -3
- package/dist/types-DbCWhHet.d.mts.map +0 -1
- package/dist/version-Dw0JXu45.mjs +0 -7
- package/dist/widgets-ClEnYQCH.mjs.map +0 -1
- /package/dist/{api-tokens-B6VgoE6M.mjs → api-tokens-Oq39ba-Z.mjs} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as withTransaction } from "./transaction-x2tJQ-A1.mjs";
|
|
2
|
-
import { a as hashApiToken, n as VALID_SCOPES, r as generatePrefixedToken, t as TOKEN_PREFIXES } from "./api-tokens-
|
|
3
|
-
import { c as validateRedirectUri, o as lookupOAuthClient, s as validateClientRedirectUri } from "./oauth-clients-
|
|
4
|
-
import { t as lookupUserRoleAndStatus } from "./oauth-user-lookup-
|
|
2
|
+
import { a as hashApiToken, n as VALID_SCOPES, r as generatePrefixedToken, t as TOKEN_PREFIXES } from "./api-tokens-Oq39ba-Z.mjs";
|
|
3
|
+
import { c as validateRedirectUri, o as lookupOAuthClient, s as validateClientRedirectUri } from "./oauth-clients-BGGFp57s.mjs";
|
|
4
|
+
import { t as lookupUserRoleAndStatus } from "./oauth-user-lookup-B_vnZHKO.mjs";
|
|
5
5
|
import { clampScopes, computeS256Challenge, secureCompare } from "@emdash-cms/auth";
|
|
6
6
|
import { generateCodeVerifier } from "arctic";
|
|
7
7
|
|
|
@@ -272,4 +272,4 @@ function buildDeniedRedirect(redirectUri, state) {
|
|
|
272
272
|
|
|
273
273
|
//#endregion
|
|
274
274
|
export { handleAuthorizationApproval as n, handleAuthorizationCodeExchange as r, buildDeniedRedirect as t };
|
|
275
|
-
//# sourceMappingURL=oauth-authorization-
|
|
275
|
+
//# sourceMappingURL=oauth-authorization-Bw4NdF_S.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth-authorization-1aPAYjiC.mjs","names":[],"sources":["../src/api/handlers/oauth-authorization.ts"],"sourcesContent":["/**\n * OAuth 2.1 Authorization Code + PKCE handlers.\n *\n * Implements the server side of the authorization code grant for MCP clients\n * (Claude Desktop, VS Code, etc.) per the MCP authorization spec (draft).\n *\n * Uses arctic for PKCE challenge generation and @emdash-cms/auth for token\n * utilities. Token infrastructure is shared with the device flow.\n */\n\nimport { clampScopes, computeS256Challenge, secureCompare } from \"@emdash-cms/auth\";\nimport type { RoleLevel } from \"@emdash-cms/auth\";\nimport { generateCodeVerifier } from \"arctic\";\nimport type { Kysely } from \"kysely\";\n\nimport {\n\tgeneratePrefixedToken,\n\thashApiToken,\n\tTOKEN_PREFIXES,\n\tVALID_SCOPES,\n} from \"../../auth/api-tokens.js\";\nimport { withTransaction } from \"../../database/transaction.js\";\nimport type { Database } from \"../../database/types.js\";\nimport { validateRedirectUri } from \"../oauth/redirect-uri.js\";\nimport type { ApiResult } from \"../types.js\";\nimport { lookupOAuthClient, validateClientRedirectUri } from \"./oauth-clients.js\";\nimport { lookupUserRoleAndStatus } from \"./oauth-user-lookup.js\";\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Authorization codes expire after 10 minutes (RFC 6749 §4.1.2 recommends short-lived) */\nconst AUTH_CODE_TTL_SECONDS = 10 * 60;\n\n/** Access token TTL: 1 hour */\nconst ACCESS_TOKEN_TTL_SECONDS = 60 * 60;\n\n/** Refresh token TTL: 90 days */\nconst REFRESH_TOKEN_TTL_SECONDS = 90 * 24 * 60 * 60;\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface AuthorizationParams {\n\tresponse_type: string;\n\tclient_id: string;\n\tredirect_uri: string;\n\tscope?: string;\n\tstate?: string;\n\tcode_challenge: string;\n\tcode_challenge_method: string;\n\tresource?: string;\n}\n\nexport interface TokenExchangeParams {\n\tgrant_type: string;\n\tcode: string;\n\tredirect_uri: string;\n\tclient_id: string;\n\tcode_verifier: string;\n\tresource?: string;\n}\n\nexport interface TokenResponse {\n\taccess_token: string;\n\trefresh_token: string;\n\ttoken_type: \"Bearer\";\n\texpires_in: number;\n\tscope: string;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction expiresAt(seconds: number): string {\n\treturn new Date(Date.now() + seconds * 1000).toISOString();\n}\n\nexport { validateRedirectUri };\n\n/**\n * Validate and normalize scopes. Returns validated scope list.\n */\nfunction normalizeScopes(requested?: string): string[] {\n\tif (!requested) return [];\n\n\tconst validSet = new Set<string>(VALID_SCOPES);\n\tconst scopes = requested\n\t\t.split(\" \")\n\t\t.filter(Boolean)\n\t\t.filter((s) => validSet.has(s));\n\n\treturn scopes;\n}\n\n// ---------------------------------------------------------------------------\n// Handlers\n// ---------------------------------------------------------------------------\n\n/**\n * Process an authorization request after the user approves consent.\n *\n * Generates an authorization code, stores it with the PKCE challenge,\n * and returns the redirect URL with the code appended.\n *\n * Scopes are clamped to the user's role to prevent scope escalation.\n */\nexport async function handleAuthorizationApproval(\n\tdb: Kysely<Database>,\n\tuserId: string,\n\tuserRole: RoleLevel,\n\tparams: AuthorizationParams,\n): Promise<ApiResult<{ redirect_url: string }>> {\n\ttry {\n\t\t// Validate response_type\n\t\tif (params.response_type !== \"code\") {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"UNSUPPORTED_RESPONSE_TYPE\",\n\t\t\t\t\tmessage: \"Only response_type=code is supported\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Validate redirect_uri scheme/host (basic security check)\n\t\tconst uriError = validateRedirectUri(params.redirect_uri);\n\t\tif (uriError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"INVALID_REDIRECT_URI\", message: uriError },\n\t\t\t};\n\t\t}\n\n\t\t// Look up the registered OAuth client\n\t\tconst client = await lookupOAuthClient(db, params.client_id);\n\t\tif (!client) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"INVALID_CLIENT\",\n\t\t\t\t\tmessage: \"Unknown client_id\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Validate redirect_uri against client's registered URIs\n\t\tconst clientUriError = validateClientRedirectUri(params.redirect_uri, client.redirectUris);\n\t\tif (clientUriError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"INVALID_REDIRECT_URI\", message: clientUriError },\n\t\t\t};\n\t\t}\n\n\t\t// Validate code_challenge_method\n\t\tif (params.code_challenge_method !== \"S256\") {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"INVALID_REQUEST\",\n\t\t\t\t\tmessage: \"Only S256 code_challenge_method is supported\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Validate code_challenge is present\n\t\tif (!params.code_challenge) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"INVALID_REQUEST\", message: \"code_challenge is required\" },\n\t\t\t};\n\t\t}\n\n\t\t// Validate scopes, then clamp to user's role\n\t\tconst userScopes = clampScopes(normalizeScopes(params.scope), userRole);\n\n\t\t// SEC-41: Intersect with client's registered scopes (if restricted).\n\t\t// A client registered with scopes: [\"content:read\"] should never receive\n\t\t// admin or schema:write, regardless of the approving user's role.\n\t\tconst clientScopes = client.scopes;\n\t\tconst scopes = clientScopes?.length\n\t\t\t? userScopes.filter((s: string) => clientScopes.includes(s))\n\t\t\t: userScopes;\n\n\t\tif (scopes.length === 0) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"INVALID_SCOPE\", message: \"No valid scopes requested\" },\n\t\t\t};\n\t\t}\n\n\t\t// Generate authorization code (high entropy, base64url)\n\t\tconst code = generateCodeVerifier(); // 32 bytes random, base64url\n\t\tconst codeHash = hashApiToken(code);\n\n\t\t// Store the authorization code\n\t\tawait db\n\t\t\t.insertInto(\"_emdash_authorization_codes\")\n\t\t\t.values({\n\t\t\t\tcode_hash: codeHash,\n\t\t\t\tclient_id: params.client_id,\n\t\t\t\tredirect_uri: params.redirect_uri,\n\t\t\t\tuser_id: userId,\n\t\t\t\tscopes: JSON.stringify(scopes),\n\t\t\t\tcode_challenge: params.code_challenge,\n\t\t\t\tcode_challenge_method: params.code_challenge_method,\n\t\t\t\tresource: params.resource ?? null,\n\t\t\t\texpires_at: expiresAt(AUTH_CODE_TTL_SECONDS),\n\t\t\t})\n\t\t\t.execute();\n\n\t\t// Build the redirect URL\n\t\tconst redirectUrl = new URL(params.redirect_uri);\n\t\tredirectUrl.searchParams.set(\"code\", code);\n\t\tif (params.state) {\n\t\t\tredirectUrl.searchParams.set(\"state\", params.state);\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { redirect_url: redirectUrl.toString() },\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"Authorization error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"AUTHORIZATION_ERROR\",\n\t\t\t\tmessage: \"Failed to process authorization\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Exchange an authorization code for access + refresh tokens.\n *\n * Validates the code, verifies PKCE, and issues tokens using the same\n * infrastructure as the device flow (ec_oat_*, ec_ort_*).\n */\nexport async function handleAuthorizationCodeExchange(\n\tdb: Kysely<Database>,\n\tparams: TokenExchangeParams,\n): Promise<ApiResult<TokenResponse>> {\n\ttry {\n\t\t// Validate grant_type\n\t\tif (params.grant_type !== \"authorization_code\") {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"unsupported_grant_type\", message: \"Invalid grant_type\" },\n\t\t\t};\n\t\t}\n\n\t\t// SEC-39: Atomically consume the authorization code using DELETE...RETURNING.\n\t\t// This prevents TOCTOU double-exchange: two concurrent requests with the\n\t\t// same code will race on the DELETE, and only one will get a row back.\n\t\tconst codeHash = hashApiToken(params.code);\n\n\t\tconst row = await db\n\t\t\t.deleteFrom(\"_emdash_authorization_codes\")\n\t\t\t.where(\"code_hash\", \"=\", codeHash)\n\t\t\t.returningAll()\n\t\t\t.executeTakeFirst();\n\n\t\tif (!row) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"invalid_grant\", message: \"Invalid authorization code\" },\n\t\t\t};\n\t\t}\n\n\t\t// Check expiry\n\t\tif (new Date(row.expires_at) < new Date()) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"invalid_grant\", message: \"Authorization code expired\" },\n\t\t\t};\n\t\t}\n\n\t\t// Verify redirect_uri matches exactly\n\t\tif (row.redirect_uri !== params.redirect_uri) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"invalid_grant\", message: \"redirect_uri mismatch\" },\n\t\t\t};\n\t\t}\n\n\t\t// Verify client_id matches\n\t\tif (row.client_id !== params.client_id) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"invalid_grant\", message: \"client_id mismatch\" },\n\t\t\t};\n\t\t}\n\n\t\t// PKCE verification: SHA256(code_verifier) must match stored code_challenge\n\t\t// Use constant-time comparison to prevent timing side-channels\n\t\tconst derivedChallenge = computeS256Challenge(params.code_verifier);\n\t\tif (!secureCompare(derivedChallenge, row.code_challenge)) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"invalid_grant\", message: \"PKCE verification failed\" },\n\t\t\t};\n\t\t}\n\n\t\t// Verify resource matches (if stored)\n\t\tif (row.resource && params.resource && row.resource !== params.resource) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"invalid_grant\", message: \"resource mismatch\" },\n\t\t\t};\n\t\t}\n\n\t\t// Revalidate user role before issuing tokens (same pattern as handleTokenRefresh).\n\t\t// The user's role may have changed since the authorization code was issued.\n\t\tconst userInfo = await lookupUserRoleAndStatus(db, row.user_id);\n\t\tif (!userInfo) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"invalid_grant\", message: \"User not found\" },\n\t\t\t};\n\t\t}\n\n\t\tif (userInfo.disabled) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"invalid_grant\", message: \"User account is disabled\" },\n\t\t\t};\n\t\t}\n\n\t\t// Re-clamp scopes against the user's current role\n\t\tconst storedScopes = JSON.parse(row.scopes) as string[];\n\t\tlet scopes = clampScopes(storedScopes, userInfo.role);\n\n\t\t// Intersect with client's registered scopes (if restricted)\n\t\tconst client = await lookupOAuthClient(db, row.client_id);\n\t\tif (client?.scopes?.length) {\n\t\t\tscopes = scopes.filter((s: string) => client.scopes!.includes(s));\n\t\t}\n\n\t\tif (scopes.length === 0) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"invalid_grant\",\n\t\t\t\t\tmessage: \"User role no longer supports any of the requested scopes\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Issue tokens (same as device flow)\n\t\tconst accessToken = generatePrefixedToken(TOKEN_PREFIXES.OAUTH_ACCESS);\n\t\tconst accessExpires = expiresAt(ACCESS_TOKEN_TTL_SECONDS);\n\n\t\tconst refreshToken = generatePrefixedToken(TOKEN_PREFIXES.OAUTH_REFRESH);\n\t\tconst refreshExpires = expiresAt(REFRESH_TOKEN_TTL_SECONDS);\n\n\t\t// Atomically store both tokens in a transaction\n\t\tawait withTransaction(db, async (trx) => {\n\t\t\tawait trx\n\t\t\t\t.insertInto(\"_emdash_oauth_tokens\")\n\t\t\t\t.values({\n\t\t\t\t\ttoken_hash: accessToken.hash,\n\t\t\t\t\ttoken_type: \"access\",\n\t\t\t\t\tuser_id: row.user_id,\n\t\t\t\t\tscopes: JSON.stringify(scopes),\n\t\t\t\t\tclient_type: \"mcp\",\n\t\t\t\t\texpires_at: accessExpires,\n\t\t\t\t\trefresh_token_hash: refreshToken.hash,\n\t\t\t\t\tclient_id: row.client_id,\n\t\t\t\t})\n\t\t\t\t.execute();\n\n\t\t\tawait trx\n\t\t\t\t.insertInto(\"_emdash_oauth_tokens\")\n\t\t\t\t.values({\n\t\t\t\t\ttoken_hash: refreshToken.hash,\n\t\t\t\t\ttoken_type: \"refresh\",\n\t\t\t\t\tuser_id: row.user_id,\n\t\t\t\t\tscopes: JSON.stringify(scopes),\n\t\t\t\t\tclient_type: \"mcp\",\n\t\t\t\t\texpires_at: refreshExpires,\n\t\t\t\t\trefresh_token_hash: null,\n\t\t\t\t\tclient_id: row.client_id,\n\t\t\t\t})\n\t\t\t\t.execute();\n\t\t});\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\taccess_token: accessToken.raw,\n\t\t\t\trefresh_token: refreshToken.raw,\n\t\t\t\ttoken_type: \"Bearer\",\n\t\t\t\texpires_in: ACCESS_TOKEN_TTL_SECONDS,\n\t\t\t\tscope: scopes.join(\" \"),\n\t\t\t},\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"Token exchange error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"TOKEN_EXCHANGE_ERROR\",\n\t\t\t\tmessage: \"Failed to exchange authorization code\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Build the authorization denied redirect URL.\n */\nexport function buildDeniedRedirect(redirectUri: string, state?: string): string {\n\tconst url = new URL(redirectUri);\n\turl.searchParams.set(\"error\", \"access_denied\");\n\turl.searchParams.set(\"error_description\", \"The user denied the authorization request\");\n\tif (state) {\n\t\turl.searchParams.set(\"state\", state);\n\t}\n\treturn url.toString();\n}\n\n/**\n * Clean up expired authorization codes.\n */\nexport async function cleanupExpiredAuthorizationCodes(db: Kysely<Database>): Promise<number> {\n\tconst result = await db\n\t\t.deleteFrom(\"_emdash_authorization_codes\")\n\t\t.where(\"expires_at\", \"<\", new Date().toISOString())\n\t\t.executeTakeFirst();\n\n\treturn Number(result.numDeletedRows);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"oauth-authorization-Bw4NdF_S.mjs","names":[],"sources":["../src/api/handlers/oauth-authorization.ts"],"sourcesContent":["/**\n * OAuth 2.1 Authorization Code + PKCE handlers.\n *\n * Implements the server side of the authorization code grant for MCP clients\n * (Claude Desktop, VS Code, etc.) per the MCP authorization spec (draft).\n *\n * Uses arctic for PKCE challenge generation and @emdash-cms/auth for token\n * utilities. Token infrastructure is shared with the device flow.\n */\n\nimport { clampScopes, computeS256Challenge, secureCompare } from \"@emdash-cms/auth\";\nimport type { RoleLevel } from \"@emdash-cms/auth\";\nimport { generateCodeVerifier } from \"arctic\";\nimport type { Kysely } from \"kysely\";\n\nimport {\n\tgeneratePrefixedToken,\n\thashApiToken,\n\tTOKEN_PREFIXES,\n\tVALID_SCOPES,\n} from \"../../auth/api-tokens.js\";\nimport { withTransaction } from \"../../database/transaction.js\";\nimport type { Database } from \"../../database/types.js\";\nimport { validateRedirectUri } from \"../oauth/redirect-uri.js\";\nimport type { ApiResult } from \"../types.js\";\nimport { lookupOAuthClient, validateClientRedirectUri } from \"./oauth-clients.js\";\nimport { lookupUserRoleAndStatus } from \"./oauth-user-lookup.js\";\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Authorization codes expire after 10 minutes (RFC 6749 §4.1.2 recommends short-lived) */\nconst AUTH_CODE_TTL_SECONDS = 10 * 60;\n\n/** Access token TTL: 1 hour */\nconst ACCESS_TOKEN_TTL_SECONDS = 60 * 60;\n\n/** Refresh token TTL: 90 days */\nconst REFRESH_TOKEN_TTL_SECONDS = 90 * 24 * 60 * 60;\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface AuthorizationParams {\n\tresponse_type: string;\n\tclient_id: string;\n\tredirect_uri: string;\n\tscope?: string;\n\tstate?: string;\n\tcode_challenge: string;\n\tcode_challenge_method: string;\n\tresource?: string;\n}\n\nexport interface TokenExchangeParams {\n\tgrant_type: string;\n\tcode: string;\n\tredirect_uri: string;\n\tclient_id: string;\n\tcode_verifier: string;\n\tresource?: string;\n}\n\nexport interface TokenResponse {\n\taccess_token: string;\n\trefresh_token: string;\n\ttoken_type: \"Bearer\";\n\texpires_in: number;\n\tscope: string;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction expiresAt(seconds: number): string {\n\treturn new Date(Date.now() + seconds * 1000).toISOString();\n}\n\nexport { validateRedirectUri };\n\n/**\n * Validate and normalize scopes. Returns validated scope list.\n */\nfunction normalizeScopes(requested?: string): string[] {\n\tif (!requested) return [];\n\n\tconst validSet = new Set<string>(VALID_SCOPES);\n\tconst scopes = requested\n\t\t.split(\" \")\n\t\t.filter(Boolean)\n\t\t.filter((s) => validSet.has(s));\n\n\treturn scopes;\n}\n\n// ---------------------------------------------------------------------------\n// Handlers\n// ---------------------------------------------------------------------------\n\n/**\n * Process an authorization request after the user approves consent.\n *\n * Generates an authorization code, stores it with the PKCE challenge,\n * and returns the redirect URL with the code appended.\n *\n * Scopes are clamped to the user's role to prevent scope escalation.\n */\nexport async function handleAuthorizationApproval(\n\tdb: Kysely<Database>,\n\tuserId: string,\n\tuserRole: RoleLevel,\n\tparams: AuthorizationParams,\n): Promise<ApiResult<{ redirect_url: string }>> {\n\ttry {\n\t\t// Validate response_type\n\t\tif (params.response_type !== \"code\") {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"UNSUPPORTED_RESPONSE_TYPE\",\n\t\t\t\t\tmessage: \"Only response_type=code is supported\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Validate redirect_uri scheme/host (basic security check)\n\t\tconst uriError = validateRedirectUri(params.redirect_uri);\n\t\tif (uriError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"INVALID_REDIRECT_URI\", message: uriError },\n\t\t\t};\n\t\t}\n\n\t\t// Look up the registered OAuth client\n\t\tconst client = await lookupOAuthClient(db, params.client_id);\n\t\tif (!client) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"INVALID_CLIENT\",\n\t\t\t\t\tmessage: \"Unknown client_id\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Validate redirect_uri against client's registered URIs\n\t\tconst clientUriError = validateClientRedirectUri(params.redirect_uri, client.redirectUris);\n\t\tif (clientUriError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"INVALID_REDIRECT_URI\", message: clientUriError },\n\t\t\t};\n\t\t}\n\n\t\t// Validate code_challenge_method\n\t\tif (params.code_challenge_method !== \"S256\") {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"INVALID_REQUEST\",\n\t\t\t\t\tmessage: \"Only S256 code_challenge_method is supported\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Validate code_challenge is present\n\t\tif (!params.code_challenge) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"INVALID_REQUEST\", message: \"code_challenge is required\" },\n\t\t\t};\n\t\t}\n\n\t\t// Validate scopes, then clamp to user's role\n\t\tconst userScopes = clampScopes(normalizeScopes(params.scope), userRole);\n\n\t\t// SEC-41: Intersect with client's registered scopes (if restricted).\n\t\t// A client registered with scopes: [\"content:read\"] should never receive\n\t\t// admin or schema:write, regardless of the approving user's role.\n\t\tconst clientScopes = client.scopes;\n\t\tconst scopes = clientScopes?.length\n\t\t\t? userScopes.filter((s: string) => clientScopes.includes(s))\n\t\t\t: userScopes;\n\n\t\tif (scopes.length === 0) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"INVALID_SCOPE\", message: \"No valid scopes requested\" },\n\t\t\t};\n\t\t}\n\n\t\t// Generate authorization code (high entropy, base64url)\n\t\tconst code = generateCodeVerifier(); // 32 bytes random, base64url\n\t\tconst codeHash = hashApiToken(code);\n\n\t\t// Store the authorization code\n\t\tawait db\n\t\t\t.insertInto(\"_emdash_authorization_codes\")\n\t\t\t.values({\n\t\t\t\tcode_hash: codeHash,\n\t\t\t\tclient_id: params.client_id,\n\t\t\t\tredirect_uri: params.redirect_uri,\n\t\t\t\tuser_id: userId,\n\t\t\t\tscopes: JSON.stringify(scopes),\n\t\t\t\tcode_challenge: params.code_challenge,\n\t\t\t\tcode_challenge_method: params.code_challenge_method,\n\t\t\t\tresource: params.resource ?? null,\n\t\t\t\texpires_at: expiresAt(AUTH_CODE_TTL_SECONDS),\n\t\t\t})\n\t\t\t.execute();\n\n\t\t// Build the redirect URL\n\t\tconst redirectUrl = new URL(params.redirect_uri);\n\t\tredirectUrl.searchParams.set(\"code\", code);\n\t\tif (params.state) {\n\t\t\tredirectUrl.searchParams.set(\"state\", params.state);\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { redirect_url: redirectUrl.toString() },\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"Authorization error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"AUTHORIZATION_ERROR\",\n\t\t\t\tmessage: \"Failed to process authorization\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Exchange an authorization code for access + refresh tokens.\n *\n * Validates the code, verifies PKCE, and issues tokens using the same\n * infrastructure as the device flow (ec_oat_*, ec_ort_*).\n */\nexport async function handleAuthorizationCodeExchange(\n\tdb: Kysely<Database>,\n\tparams: TokenExchangeParams,\n): Promise<ApiResult<TokenResponse>> {\n\ttry {\n\t\t// Validate grant_type\n\t\tif (params.grant_type !== \"authorization_code\") {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"unsupported_grant_type\", message: \"Invalid grant_type\" },\n\t\t\t};\n\t\t}\n\n\t\t// SEC-39: Atomically consume the authorization code using DELETE...RETURNING.\n\t\t// This prevents TOCTOU double-exchange: two concurrent requests with the\n\t\t// same code will race on the DELETE, and only one will get a row back.\n\t\tconst codeHash = hashApiToken(params.code);\n\n\t\tconst row = await db\n\t\t\t.deleteFrom(\"_emdash_authorization_codes\")\n\t\t\t.where(\"code_hash\", \"=\", codeHash)\n\t\t\t.returningAll()\n\t\t\t.executeTakeFirst();\n\n\t\tif (!row) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"invalid_grant\", message: \"Invalid authorization code\" },\n\t\t\t};\n\t\t}\n\n\t\t// Check expiry\n\t\tif (new Date(row.expires_at) < new Date()) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"invalid_grant\", message: \"Authorization code expired\" },\n\t\t\t};\n\t\t}\n\n\t\t// Verify redirect_uri matches exactly\n\t\tif (row.redirect_uri !== params.redirect_uri) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"invalid_grant\", message: \"redirect_uri mismatch\" },\n\t\t\t};\n\t\t}\n\n\t\t// Verify client_id matches\n\t\tif (row.client_id !== params.client_id) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"invalid_grant\", message: \"client_id mismatch\" },\n\t\t\t};\n\t\t}\n\n\t\t// PKCE verification: SHA256(code_verifier) must match stored code_challenge\n\t\t// Use constant-time comparison to prevent timing side-channels\n\t\tconst derivedChallenge = computeS256Challenge(params.code_verifier);\n\t\tif (!secureCompare(derivedChallenge, row.code_challenge)) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"invalid_grant\", message: \"PKCE verification failed\" },\n\t\t\t};\n\t\t}\n\n\t\t// Verify resource matches (if stored)\n\t\tif (row.resource && params.resource && row.resource !== params.resource) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"invalid_grant\", message: \"resource mismatch\" },\n\t\t\t};\n\t\t}\n\n\t\t// Revalidate user role before issuing tokens (same pattern as handleTokenRefresh).\n\t\t// The user's role may have changed since the authorization code was issued.\n\t\tconst userInfo = await lookupUserRoleAndStatus(db, row.user_id);\n\t\tif (!userInfo) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"invalid_grant\", message: \"User not found\" },\n\t\t\t};\n\t\t}\n\n\t\tif (userInfo.disabled) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"invalid_grant\", message: \"User account is disabled\" },\n\t\t\t};\n\t\t}\n\n\t\t// Re-clamp scopes against the user's current role\n\t\tconst storedScopes = JSON.parse(row.scopes) as string[];\n\t\tlet scopes = clampScopes(storedScopes, userInfo.role);\n\n\t\t// Intersect with client's registered scopes (if restricted)\n\t\tconst client = await lookupOAuthClient(db, row.client_id);\n\t\tif (client?.scopes?.length) {\n\t\t\tscopes = scopes.filter((s: string) => client.scopes!.includes(s));\n\t\t}\n\n\t\tif (scopes.length === 0) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"invalid_grant\",\n\t\t\t\t\tmessage: \"User role no longer supports any of the requested scopes\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Issue tokens (same as device flow)\n\t\tconst accessToken = generatePrefixedToken(TOKEN_PREFIXES.OAUTH_ACCESS);\n\t\tconst accessExpires = expiresAt(ACCESS_TOKEN_TTL_SECONDS);\n\n\t\tconst refreshToken = generatePrefixedToken(TOKEN_PREFIXES.OAUTH_REFRESH);\n\t\tconst refreshExpires = expiresAt(REFRESH_TOKEN_TTL_SECONDS);\n\n\t\t// Atomically store both tokens in a transaction\n\t\tawait withTransaction(db, async (trx) => {\n\t\t\tawait trx\n\t\t\t\t.insertInto(\"_emdash_oauth_tokens\")\n\t\t\t\t.values({\n\t\t\t\t\ttoken_hash: accessToken.hash,\n\t\t\t\t\ttoken_type: \"access\",\n\t\t\t\t\tuser_id: row.user_id,\n\t\t\t\t\tscopes: JSON.stringify(scopes),\n\t\t\t\t\tclient_type: \"mcp\",\n\t\t\t\t\texpires_at: accessExpires,\n\t\t\t\t\trefresh_token_hash: refreshToken.hash,\n\t\t\t\t\tclient_id: row.client_id,\n\t\t\t\t})\n\t\t\t\t.execute();\n\n\t\t\tawait trx\n\t\t\t\t.insertInto(\"_emdash_oauth_tokens\")\n\t\t\t\t.values({\n\t\t\t\t\ttoken_hash: refreshToken.hash,\n\t\t\t\t\ttoken_type: \"refresh\",\n\t\t\t\t\tuser_id: row.user_id,\n\t\t\t\t\tscopes: JSON.stringify(scopes),\n\t\t\t\t\tclient_type: \"mcp\",\n\t\t\t\t\texpires_at: refreshExpires,\n\t\t\t\t\trefresh_token_hash: null,\n\t\t\t\t\tclient_id: row.client_id,\n\t\t\t\t})\n\t\t\t\t.execute();\n\t\t});\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\taccess_token: accessToken.raw,\n\t\t\t\trefresh_token: refreshToken.raw,\n\t\t\t\ttoken_type: \"Bearer\",\n\t\t\t\texpires_in: ACCESS_TOKEN_TTL_SECONDS,\n\t\t\t\tscope: scopes.join(\" \"),\n\t\t\t},\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"Token exchange error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"TOKEN_EXCHANGE_ERROR\",\n\t\t\t\tmessage: \"Failed to exchange authorization code\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Build the authorization denied redirect URL.\n */\nexport function buildDeniedRedirect(redirectUri: string, state?: string): string {\n\tconst url = new URL(redirectUri);\n\turl.searchParams.set(\"error\", \"access_denied\");\n\turl.searchParams.set(\"error_description\", \"The user denied the authorization request\");\n\tif (state) {\n\t\turl.searchParams.set(\"state\", state);\n\t}\n\treturn url.toString();\n}\n\n/**\n * Clean up expired authorization codes.\n */\nexport async function cleanupExpiredAuthorizationCodes(db: Kysely<Database>): Promise<number> {\n\tconst result = await db\n\t\t.deleteFrom(\"_emdash_authorization_codes\")\n\t\t.where(\"expires_at\", \"<\", new Date().toISOString())\n\t\t.executeTakeFirst();\n\n\treturn Number(result.numDeletedRows);\n}\n"],"mappings":"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"}
|
|
@@ -263,4 +263,4 @@ function validateClientRedirectUri(redirectUri, allowedUris) {
|
|
|
263
263
|
|
|
264
264
|
//#endregion
|
|
265
265
|
export { handleOAuthClientUpdate as a, validateRedirectUri as c, handleOAuthClientList as i, handleOAuthClientDelete as n, lookupOAuthClient as o, handleOAuthClientGet as r, validateClientRedirectUri as s, handleOAuthClientCreate as t };
|
|
266
|
-
//# sourceMappingURL=oauth-clients-
|
|
266
|
+
//# sourceMappingURL=oauth-clients-BGGFp57s.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth-clients-8mPDStMv.mjs","names":[],"sources":["../src/api/oauth/redirect-uri.ts","../src/api/handlers/oauth-clients.ts"],"sourcesContent":["/**\n * Validate a redirect URI per OAuth 2.1 security requirements.\n *\n * Allows localhost / loopback redirect URIs over HTTP for native clients,\n * and any HTTPS URL for web-based flows.\n */\nexport function validateRedirectUri(uri: string): string | null {\n\ttry {\n\t\tconst url = new URL(uri);\n\n\t\t// Reject protocol-relative URLs\n\t\tif (uri.startsWith(\"//\")) {\n\t\t\treturn \"Protocol-relative redirect URIs are not allowed\";\n\t\t}\n\n\t\t// Allow localhost/loopback over HTTP (for desktop MCP clients)\n\t\tif (url.protocol === \"http:\") {\n\t\t\tconst host = url.hostname;\n\t\t\tif (host === \"127.0.0.1\" || host === \"localhost\" || host === \"[::1]\") {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\treturn \"HTTP redirect URIs are only allowed for localhost\";\n\t\t}\n\n\t\t// Allow HTTPS\n\t\tif (url.protocol === \"https:\") {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn `Unsupported redirect URI scheme: ${url.protocol}`;\n\t} catch {\n\t\treturn \"Invalid redirect URI\";\n\t}\n}\n","/**\n * OAuth client management handlers.\n *\n * CRUD operations for registered OAuth clients. Each client has a set\n * of pre-registered redirect URIs. The authorization endpoint rejects\n * any redirect_uri not in the client's registered set.\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport type { Database } from \"../../database/types.js\";\nimport { validateRedirectUri } from \"../oauth/redirect-uri.js\";\nimport type { ApiResult } from \"../types.js\";\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Parse a JSON string column into a typed value. */\nfunction parseJsonColumn<T>(value: string): T {\n\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- JSON.parse returns unknown, callers provide the expected shape\n\treturn JSON.parse(value) as T;\n}\n\nfunction validateRegisteredRedirectUris(redirectUris: string[]): string | null {\n\tfor (const redirectUri of redirectUris) {\n\t\tconst error = validateRedirectUri(redirectUri);\n\t\tif (error) {\n\t\t\treturn `Invalid redirect URI: ${error}`;\n\t\t}\n\t}\n\treturn null;\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface OAuthClientInfo {\n\tid: string;\n\tname: string;\n\tredirectUris: string[];\n\tscopes: string[] | null;\n\tcreatedAt: string;\n\tupdatedAt: string;\n}\n\n// ---------------------------------------------------------------------------\n// Handlers\n// ---------------------------------------------------------------------------\n\n/**\n * Create a new OAuth client.\n */\nexport async function handleOAuthClientCreate(\n\tdb: Kysely<Database>,\n\tinput: {\n\t\tid: string;\n\t\tname: string;\n\t\tredirectUris: string[];\n\t\tscopes?: string[] | null;\n\t},\n): Promise<ApiResult<OAuthClientInfo>> {\n\ttry {\n\t\tif (input.redirectUris.length === 0) {\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: \"At least one redirect URI is required\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst redirectUriError = validateRegisteredRedirectUris(input.redirectUris);\n\t\tif (redirectUriError) {\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: redirectUriError,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Check for duplicate client ID\n\t\tconst existing = await db\n\t\t\t.selectFrom(\"_emdash_oauth_clients\")\n\t\t\t.select(\"id\")\n\t\t\t.where(\"id\", \"=\", input.id)\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: \"CONFLICT\", message: \"OAuth client with this ID already exists\" },\n\t\t\t};\n\t\t}\n\n\t\tconst now = new Date().toISOString();\n\n\t\tawait db\n\t\t\t.insertInto(\"_emdash_oauth_clients\")\n\t\t\t.values({\n\t\t\t\tid: input.id,\n\t\t\t\tname: input.name,\n\t\t\t\tredirect_uris: JSON.stringify(input.redirectUris),\n\t\t\t\tscopes: input.scopes && input.scopes.length > 0 ? JSON.stringify(input.scopes) : null,\n\t\t\t})\n\t\t\t.execute();\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\tid: input.id,\n\t\t\t\tname: input.name,\n\t\t\t\tredirectUris: input.redirectUris,\n\t\t\t\tscopes: input.scopes && input.scopes.length > 0 ? input.scopes : null,\n\t\t\t\tcreatedAt: now,\n\t\t\t\tupdatedAt: now,\n\t\t\t},\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CLIENT_CREATE_ERROR\",\n\t\t\t\tmessage: \"Failed to create OAuth client\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * List all registered OAuth clients.\n */\nexport async function handleOAuthClientList(\n\tdb: Kysely<Database>,\n): Promise<ApiResult<{ items: OAuthClientInfo[] }>> {\n\ttry {\n\t\tconst rows = await db\n\t\t\t.selectFrom(\"_emdash_oauth_clients\")\n\t\t\t.selectAll()\n\t\t\t.orderBy(\"created_at\", \"desc\")\n\t\t\t.execute();\n\n\t\tconst items: OAuthClientInfo[] = rows.map((row) => ({\n\t\t\tid: row.id,\n\t\t\tname: row.name,\n\t\t\tredirectUris: parseJsonColumn<string[]>(row.redirect_uris),\n\t\t\tscopes: row.scopes ? parseJsonColumn<string[]>(row.scopes) : null,\n\t\t\tcreatedAt: row.created_at,\n\t\t\tupdatedAt: row.updated_at,\n\t\t}));\n\n\t\treturn { success: true, data: { items } };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CLIENT_LIST_ERROR\",\n\t\t\t\tmessage: \"Failed to list OAuth clients\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Get a single OAuth client by ID.\n */\nexport async function handleOAuthClientGet(\n\tdb: Kysely<Database>,\n\tclientId: string,\n): Promise<ApiResult<OAuthClientInfo>> {\n\ttry {\n\t\tconst row = await db\n\t\t\t.selectFrom(\"_emdash_oauth_clients\")\n\t\t\t.selectAll()\n\t\t\t.where(\"id\", \"=\", clientId)\n\t\t\t.executeTakeFirst();\n\n\t\tif (!row) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: \"OAuth client not found\" },\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\tid: row.id,\n\t\t\t\tname: row.name,\n\t\t\t\tredirectUris: parseJsonColumn<string[]>(row.redirect_uris),\n\t\t\t\tscopes: row.scopes ? parseJsonColumn<string[]>(row.scopes) : null,\n\t\t\t\tcreatedAt: row.created_at,\n\t\t\t\tupdatedAt: row.updated_at,\n\t\t\t},\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CLIENT_GET_ERROR\",\n\t\t\t\tmessage: \"Failed to get OAuth client\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Update an OAuth client.\n */\nexport async function handleOAuthClientUpdate(\n\tdb: Kysely<Database>,\n\tclientId: string,\n\tinput: {\n\t\tname?: string;\n\t\tredirectUris?: string[];\n\t\tscopes?: string[] | null;\n\t},\n): Promise<ApiResult<OAuthClientInfo>> {\n\ttry {\n\t\tconst existing = await db\n\t\t\t.selectFrom(\"_emdash_oauth_clients\")\n\t\t\t.selectAll()\n\t\t\t.where(\"id\", \"=\", clientId)\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: \"OAuth client not found\" },\n\t\t\t};\n\t\t}\n\n\t\tif (input.redirectUris !== undefined && input.redirectUris.length === 0) {\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: \"At least one redirect URI is required\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tif (input.redirectUris !== undefined) {\n\t\t\tconst redirectUriError = validateRegisteredRedirectUris(input.redirectUris);\n\t\t\tif (redirectUriError) {\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: redirectUriError,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tconst updates: Record<string, string | null> = {\n\t\t\tupdated_at: new Date().toISOString(),\n\t\t};\n\n\t\tif (input.name !== undefined) {\n\t\t\tupdates.name = input.name;\n\t\t}\n\t\tif (input.redirectUris !== undefined) {\n\t\t\tupdates.redirect_uris = JSON.stringify(input.redirectUris);\n\t\t}\n\t\tif (input.scopes !== undefined) {\n\t\t\tupdates.scopes =\n\t\t\t\tinput.scopes && input.scopes.length > 0 ? JSON.stringify(input.scopes) : null;\n\t\t}\n\n\t\tawait db.updateTable(\"_emdash_oauth_clients\").set(updates).where(\"id\", \"=\", clientId).execute();\n\n\t\t// Fetch the updated row\n\t\tconst updated = await db\n\t\t\t.selectFrom(\"_emdash_oauth_clients\")\n\t\t\t.selectAll()\n\t\t\t.where(\"id\", \"=\", clientId)\n\t\t\t.executeTakeFirst();\n\n\t\tif (!updated) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: \"OAuth client not found after update\" },\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\tid: updated.id,\n\t\t\t\tname: updated.name,\n\t\t\t\tredirectUris: parseJsonColumn<string[]>(updated.redirect_uris),\n\t\t\t\tscopes: updated.scopes ? parseJsonColumn<string[]>(updated.scopes) : null,\n\t\t\t\tcreatedAt: updated.created_at,\n\t\t\t\tupdatedAt: updated.updated_at,\n\t\t\t},\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CLIENT_UPDATE_ERROR\",\n\t\t\t\tmessage: \"Failed to update OAuth client\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Delete an OAuth client.\n */\nexport async function handleOAuthClientDelete(\n\tdb: Kysely<Database>,\n\tclientId: string,\n): Promise<ApiResult<{ deleted: true }>> {\n\ttry {\n\t\tconst result = await db\n\t\t\t.deleteFrom(\"_emdash_oauth_clients\")\n\t\t\t.where(\"id\", \"=\", clientId)\n\t\t\t.executeTakeFirst();\n\n\t\tif (result.numDeletedRows === 0n) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: \"OAuth client not found\" },\n\t\t\t};\n\t\t}\n\n\t\treturn { success: true, data: { deleted: true } };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CLIENT_DELETE_ERROR\",\n\t\t\t\tmessage: \"Failed to delete OAuth client\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Lookup helpers (used by authorization handler)\n// ---------------------------------------------------------------------------\n\n/**\n * Look up a registered OAuth client by ID.\n * Returns the client's redirect URIs or null if the client is not registered.\n */\nexport async function lookupOAuthClient(\n\tdb: Kysely<Database>,\n\tclientId: string,\n): Promise<{ redirectUris: string[]; scopes: string[] | null } | null> {\n\tconst row = await db\n\t\t.selectFrom(\"_emdash_oauth_clients\")\n\t\t.select([\"redirect_uris\", \"scopes\"])\n\t\t.where(\"id\", \"=\", clientId)\n\t\t.executeTakeFirst();\n\n\tif (!row) return null;\n\n\treturn {\n\t\tredirectUris: parseJsonColumn<string[]>(row.redirect_uris),\n\t\tscopes: row.scopes ? parseJsonColumn<string[]>(row.scopes) : null,\n\t};\n}\n\n/**\n * Validate that a redirect URI is in the client's registered set.\n *\n * Comparison is exact string match (per RFC 6749 §3.1.2.3).\n * Returns null if valid, or an error message if not.\n */\nexport function validateClientRedirectUri(\n\tredirectUri: string,\n\tallowedUris: string[],\n): string | null {\n\tif (allowedUris.includes(redirectUri)) {\n\t\treturn null; // OK\n\t}\n\treturn \"redirect_uri is not registered for this client\";\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"oauth-clients-BGGFp57s.mjs","names":[],"sources":["../src/api/oauth/redirect-uri.ts","../src/api/handlers/oauth-clients.ts"],"sourcesContent":["/**\n * Validate a redirect URI per OAuth 2.1 security requirements.\n *\n * Allows localhost / loopback redirect URIs over HTTP for native clients,\n * and any HTTPS URL for web-based flows.\n */\nexport function validateRedirectUri(uri: string): string | null {\n\ttry {\n\t\tconst url = new URL(uri);\n\n\t\t// Reject protocol-relative URLs\n\t\tif (uri.startsWith(\"//\")) {\n\t\t\treturn \"Protocol-relative redirect URIs are not allowed\";\n\t\t}\n\n\t\t// Allow localhost/loopback over HTTP (for desktop MCP clients)\n\t\tif (url.protocol === \"http:\") {\n\t\t\tconst host = url.hostname;\n\t\t\tif (host === \"127.0.0.1\" || host === \"localhost\" || host === \"[::1]\") {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\treturn \"HTTP redirect URIs are only allowed for localhost\";\n\t\t}\n\n\t\t// Allow HTTPS\n\t\tif (url.protocol === \"https:\") {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn `Unsupported redirect URI scheme: ${url.protocol}`;\n\t} catch {\n\t\treturn \"Invalid redirect URI\";\n\t}\n}\n","/**\n * OAuth client management handlers.\n *\n * CRUD operations for registered OAuth clients. Each client has a set\n * of pre-registered redirect URIs. The authorization endpoint rejects\n * any redirect_uri not in the client's registered set.\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport type { Database } from \"../../database/types.js\";\nimport { validateRedirectUri } from \"../oauth/redirect-uri.js\";\nimport type { ApiResult } from \"../types.js\";\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Parse a JSON string column into a typed value. */\nfunction parseJsonColumn<T>(value: string): T {\n\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- JSON.parse returns unknown, callers provide the expected shape\n\treturn JSON.parse(value) as T;\n}\n\nfunction validateRegisteredRedirectUris(redirectUris: string[]): string | null {\n\tfor (const redirectUri of redirectUris) {\n\t\tconst error = validateRedirectUri(redirectUri);\n\t\tif (error) {\n\t\t\treturn `Invalid redirect URI: ${error}`;\n\t\t}\n\t}\n\treturn null;\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface OAuthClientInfo {\n\tid: string;\n\tname: string;\n\tredirectUris: string[];\n\tscopes: string[] | null;\n\tcreatedAt: string;\n\tupdatedAt: string;\n}\n\n// ---------------------------------------------------------------------------\n// Handlers\n// ---------------------------------------------------------------------------\n\n/**\n * Create a new OAuth client.\n */\nexport async function handleOAuthClientCreate(\n\tdb: Kysely<Database>,\n\tinput: {\n\t\tid: string;\n\t\tname: string;\n\t\tredirectUris: string[];\n\t\tscopes?: string[] | null;\n\t},\n): Promise<ApiResult<OAuthClientInfo>> {\n\ttry {\n\t\tif (input.redirectUris.length === 0) {\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: \"At least one redirect URI is required\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst redirectUriError = validateRegisteredRedirectUris(input.redirectUris);\n\t\tif (redirectUriError) {\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: redirectUriError,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Check for duplicate client ID\n\t\tconst existing = await db\n\t\t\t.selectFrom(\"_emdash_oauth_clients\")\n\t\t\t.select(\"id\")\n\t\t\t.where(\"id\", \"=\", input.id)\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: \"CONFLICT\", message: \"OAuth client with this ID already exists\" },\n\t\t\t};\n\t\t}\n\n\t\tconst now = new Date().toISOString();\n\n\t\tawait db\n\t\t\t.insertInto(\"_emdash_oauth_clients\")\n\t\t\t.values({\n\t\t\t\tid: input.id,\n\t\t\t\tname: input.name,\n\t\t\t\tredirect_uris: JSON.stringify(input.redirectUris),\n\t\t\t\tscopes: input.scopes && input.scopes.length > 0 ? JSON.stringify(input.scopes) : null,\n\t\t\t})\n\t\t\t.execute();\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\tid: input.id,\n\t\t\t\tname: input.name,\n\t\t\t\tredirectUris: input.redirectUris,\n\t\t\t\tscopes: input.scopes && input.scopes.length > 0 ? input.scopes : null,\n\t\t\t\tcreatedAt: now,\n\t\t\t\tupdatedAt: now,\n\t\t\t},\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CLIENT_CREATE_ERROR\",\n\t\t\t\tmessage: \"Failed to create OAuth client\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * List all registered OAuth clients.\n */\nexport async function handleOAuthClientList(\n\tdb: Kysely<Database>,\n): Promise<ApiResult<{ items: OAuthClientInfo[] }>> {\n\ttry {\n\t\tconst rows = await db\n\t\t\t.selectFrom(\"_emdash_oauth_clients\")\n\t\t\t.selectAll()\n\t\t\t.orderBy(\"created_at\", \"desc\")\n\t\t\t.execute();\n\n\t\tconst items: OAuthClientInfo[] = rows.map((row) => ({\n\t\t\tid: row.id,\n\t\t\tname: row.name,\n\t\t\tredirectUris: parseJsonColumn<string[]>(row.redirect_uris),\n\t\t\tscopes: row.scopes ? parseJsonColumn<string[]>(row.scopes) : null,\n\t\t\tcreatedAt: row.created_at,\n\t\t\tupdatedAt: row.updated_at,\n\t\t}));\n\n\t\treturn { success: true, data: { items } };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CLIENT_LIST_ERROR\",\n\t\t\t\tmessage: \"Failed to list OAuth clients\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Get a single OAuth client by ID.\n */\nexport async function handleOAuthClientGet(\n\tdb: Kysely<Database>,\n\tclientId: string,\n): Promise<ApiResult<OAuthClientInfo>> {\n\ttry {\n\t\tconst row = await db\n\t\t\t.selectFrom(\"_emdash_oauth_clients\")\n\t\t\t.selectAll()\n\t\t\t.where(\"id\", \"=\", clientId)\n\t\t\t.executeTakeFirst();\n\n\t\tif (!row) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: \"OAuth client not found\" },\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\tid: row.id,\n\t\t\t\tname: row.name,\n\t\t\t\tredirectUris: parseJsonColumn<string[]>(row.redirect_uris),\n\t\t\t\tscopes: row.scopes ? parseJsonColumn<string[]>(row.scopes) : null,\n\t\t\t\tcreatedAt: row.created_at,\n\t\t\t\tupdatedAt: row.updated_at,\n\t\t\t},\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CLIENT_GET_ERROR\",\n\t\t\t\tmessage: \"Failed to get OAuth client\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Update an OAuth client.\n */\nexport async function handleOAuthClientUpdate(\n\tdb: Kysely<Database>,\n\tclientId: string,\n\tinput: {\n\t\tname?: string;\n\t\tredirectUris?: string[];\n\t\tscopes?: string[] | null;\n\t},\n): Promise<ApiResult<OAuthClientInfo>> {\n\ttry {\n\t\tconst existing = await db\n\t\t\t.selectFrom(\"_emdash_oauth_clients\")\n\t\t\t.selectAll()\n\t\t\t.where(\"id\", \"=\", clientId)\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: \"OAuth client not found\" },\n\t\t\t};\n\t\t}\n\n\t\tif (input.redirectUris !== undefined && input.redirectUris.length === 0) {\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: \"At least one redirect URI is required\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tif (input.redirectUris !== undefined) {\n\t\t\tconst redirectUriError = validateRegisteredRedirectUris(input.redirectUris);\n\t\t\tif (redirectUriError) {\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: redirectUriError,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tconst updates: Record<string, string | null> = {\n\t\t\tupdated_at: new Date().toISOString(),\n\t\t};\n\n\t\tif (input.name !== undefined) {\n\t\t\tupdates.name = input.name;\n\t\t}\n\t\tif (input.redirectUris !== undefined) {\n\t\t\tupdates.redirect_uris = JSON.stringify(input.redirectUris);\n\t\t}\n\t\tif (input.scopes !== undefined) {\n\t\t\tupdates.scopes =\n\t\t\t\tinput.scopes && input.scopes.length > 0 ? JSON.stringify(input.scopes) : null;\n\t\t}\n\n\t\tawait db.updateTable(\"_emdash_oauth_clients\").set(updates).where(\"id\", \"=\", clientId).execute();\n\n\t\t// Fetch the updated row\n\t\tconst updated = await db\n\t\t\t.selectFrom(\"_emdash_oauth_clients\")\n\t\t\t.selectAll()\n\t\t\t.where(\"id\", \"=\", clientId)\n\t\t\t.executeTakeFirst();\n\n\t\tif (!updated) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: \"OAuth client not found after update\" },\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\tid: updated.id,\n\t\t\t\tname: updated.name,\n\t\t\t\tredirectUris: parseJsonColumn<string[]>(updated.redirect_uris),\n\t\t\t\tscopes: updated.scopes ? parseJsonColumn<string[]>(updated.scopes) : null,\n\t\t\t\tcreatedAt: updated.created_at,\n\t\t\t\tupdatedAt: updated.updated_at,\n\t\t\t},\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CLIENT_UPDATE_ERROR\",\n\t\t\t\tmessage: \"Failed to update OAuth client\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Delete an OAuth client.\n */\nexport async function handleOAuthClientDelete(\n\tdb: Kysely<Database>,\n\tclientId: string,\n): Promise<ApiResult<{ deleted: true }>> {\n\ttry {\n\t\tconst result = await db\n\t\t\t.deleteFrom(\"_emdash_oauth_clients\")\n\t\t\t.where(\"id\", \"=\", clientId)\n\t\t\t.executeTakeFirst();\n\n\t\tif (result.numDeletedRows === 0n) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: \"OAuth client not found\" },\n\t\t\t};\n\t\t}\n\n\t\treturn { success: true, data: { deleted: true } };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CLIENT_DELETE_ERROR\",\n\t\t\t\tmessage: \"Failed to delete OAuth client\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Lookup helpers (used by authorization handler)\n// ---------------------------------------------------------------------------\n\n/**\n * Look up a registered OAuth client by ID.\n * Returns the client's redirect URIs or null if the client is not registered.\n */\nexport async function lookupOAuthClient(\n\tdb: Kysely<Database>,\n\tclientId: string,\n): Promise<{ redirectUris: string[]; scopes: string[] | null } | null> {\n\tconst row = await db\n\t\t.selectFrom(\"_emdash_oauth_clients\")\n\t\t.select([\"redirect_uris\", \"scopes\"])\n\t\t.where(\"id\", \"=\", clientId)\n\t\t.executeTakeFirst();\n\n\tif (!row) return null;\n\n\treturn {\n\t\tredirectUris: parseJsonColumn<string[]>(row.redirect_uris),\n\t\tscopes: row.scopes ? parseJsonColumn<string[]>(row.scopes) : null,\n\t};\n}\n\n/**\n * Validate that a redirect URI is in the client's registered set.\n *\n * Comparison is exact string match (per RFC 6749 §3.1.2.3).\n * Returns null if valid, or an error message if not.\n */\nexport function validateClientRedirectUri(\n\tredirectUri: string,\n\tallowedUris: string[],\n): string | null {\n\tif (allowedUris.includes(redirectUri)) {\n\t\treturn null; // OK\n\t}\n\treturn \"redirect_uri is not registered for this client\";\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth-state-store-
|
|
1
|
+
{"version":3,"file":"oauth-state-store-97x0xtN2.mjs","names":[],"sources":["../src/auth/oauth-state-store.ts"],"sourcesContent":["/**\n * OAuth state store\n *\n * Stores OAuth state in the auth_challenges table with automatic expiration.\n * Uses the existing table but with type=\"oauth\" to distinguish from WebAuthn challenges.\n */\n\nimport type { StateStore, OAuthState } from \"@emdash-cms/auth\";\nimport type { Kysely } from \"kysely\";\n\nimport type { Database } from \"../database/types.js\";\n\nconst OAUTH_STATE_TTL_MS = 10 * 60 * 1000; // 10 minutes\n\nexport function createOAuthStateStore(db: Kysely<Database>): StateStore {\n\treturn {\n\t\tasync set(state: string, data: OAuthState): Promise<void> {\n\t\t\tconst expiresAt = new Date(Date.now() + OAUTH_STATE_TTL_MS).toISOString();\n\n\t\t\tawait db\n\t\t\t\t.insertInto(\"auth_challenges\")\n\t\t\t\t.values({\n\t\t\t\t\tchallenge: state,\n\t\t\t\t\ttype: \"oauth\",\n\t\t\t\t\tuser_id: null,\n\t\t\t\t\tdata: JSON.stringify(data),\n\t\t\t\t\texpires_at: expiresAt,\n\t\t\t\t})\n\t\t\t\t.onConflict((oc) =>\n\t\t\t\t\toc.column(\"challenge\").doUpdateSet({\n\t\t\t\t\t\ttype: \"oauth\",\n\t\t\t\t\t\tdata: JSON.stringify(data),\n\t\t\t\t\t\texpires_at: expiresAt,\n\t\t\t\t\t}),\n\t\t\t\t)\n\t\t\t\t.execute();\n\t\t},\n\n\t\tasync get(state: string): Promise<OAuthState | null> {\n\t\t\tconst row = await db\n\t\t\t\t.selectFrom(\"auth_challenges\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"challenge\", \"=\", state)\n\t\t\t\t.where(\"type\", \"=\", \"oauth\")\n\t\t\t\t.executeTakeFirst();\n\n\t\t\tif (!row) return null;\n\n\t\t\tconst expiresAt = new Date(row.expires_at).getTime();\n\n\t\t\t// Check expiration\n\t\t\tif (expiresAt < Date.now()) {\n\t\t\t\t// Expired, delete and return null\n\t\t\t\tawait this.delete(state);\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tif (!row.data) return null;\n\n\t\t\ttry {\n\t\t\t\tconst parsed: unknown = JSON.parse(row.data);\n\t\t\t\tif (\n\t\t\t\t\ttypeof parsed !== \"object\" ||\n\t\t\t\t\tparsed === null ||\n\t\t\t\t\t!(\"provider\" in parsed) ||\n\t\t\t\t\ttypeof parsed.provider !== \"string\" ||\n\t\t\t\t\t!(\"redirectUri\" in parsed) ||\n\t\t\t\t\ttypeof parsed.redirectUri !== \"string\"\n\t\t\t\t) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\t\t\t\tconst oauthState: OAuthState = {\n\t\t\t\t\tprovider: parsed.provider,\n\t\t\t\t\tredirectUri: parsed.redirectUri,\n\t\t\t\t};\n\t\t\t\tif (\"codeVerifier\" in parsed && typeof parsed.codeVerifier === \"string\") {\n\t\t\t\t\toauthState.codeVerifier = parsed.codeVerifier;\n\t\t\t\t}\n\t\t\t\tif (\"nonce\" in parsed && typeof parsed.nonce === \"string\") {\n\t\t\t\t\toauthState.nonce = parsed.nonce;\n\t\t\t\t}\n\t\t\t\treturn oauthState;\n\t\t\t} catch {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t},\n\n\t\tasync delete(state: string): Promise<void> {\n\t\t\tawait db\n\t\t\t\t.deleteFrom(\"auth_challenges\")\n\t\t\t\t.where(\"challenge\", \"=\", state)\n\t\t\t\t.where(\"type\", \"=\", \"oauth\")\n\t\t\t\t.execute();\n\t\t},\n\t};\n}\n"],"mappings":";AAYA,MAAM,qBAAqB,MAAU;AAErC,SAAgB,sBAAsB,IAAkC;AACvE,QAAO;EACN,MAAM,IAAI,OAAe,MAAiC;GACzD,MAAM,YAAY,IAAI,KAAK,KAAK,KAAK,GAAG,mBAAmB,CAAC,aAAa;AAEzE,SAAM,GACJ,WAAW,kBAAkB,CAC7B,OAAO;IACP,WAAW;IACX,MAAM;IACN,SAAS;IACT,MAAM,KAAK,UAAU,KAAK;IAC1B,YAAY;IACZ,CAAC,CACD,YAAY,OACZ,GAAG,OAAO,YAAY,CAAC,YAAY;IAClC,MAAM;IACN,MAAM,KAAK,UAAU,KAAK;IAC1B,YAAY;IACZ,CAAC,CACF,CACA,SAAS;;EAGZ,MAAM,IAAI,OAA2C;GACpD,MAAM,MAAM,MAAM,GAChB,WAAW,kBAAkB,CAC7B,WAAW,CACX,MAAM,aAAa,KAAK,MAAM,CAC9B,MAAM,QAAQ,KAAK,QAAQ,CAC3B,kBAAkB;AAEpB,OAAI,CAAC,IAAK,QAAO;AAKjB,OAHkB,IAAI,KAAK,IAAI,WAAW,CAAC,SAAS,GAGpC,KAAK,KAAK,EAAE;AAE3B,UAAM,KAAK,OAAO,MAAM;AACxB,WAAO;;AAGR,OAAI,CAAC,IAAI,KAAM,QAAO;AAEtB,OAAI;IACH,MAAM,SAAkB,KAAK,MAAM,IAAI,KAAK;AAC5C,QACC,OAAO,WAAW,YAClB,WAAW,QACX,EAAE,cAAc,WAChB,OAAO,OAAO,aAAa,YAC3B,EAAE,iBAAiB,WACnB,OAAO,OAAO,gBAAgB,SAE9B,QAAO;IAER,MAAM,aAAyB;KAC9B,UAAU,OAAO;KACjB,aAAa,OAAO;KACpB;AACD,QAAI,kBAAkB,UAAU,OAAO,OAAO,iBAAiB,SAC9D,YAAW,eAAe,OAAO;AAElC,QAAI,WAAW,UAAU,OAAO,OAAO,UAAU,SAChD,YAAW,QAAQ,OAAO;AAE3B,WAAO;WACA;AACP,WAAO;;;EAIT,MAAM,OAAO,OAA8B;AAC1C,SAAM,GACJ,WAAW,kBAAkB,CAC7B,MAAM,aAAa,KAAK,MAAM,CAC9B,MAAM,QAAQ,KAAK,QAAQ,CAC3B,SAAS;;EAEZ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth-user-lookup-
|
|
1
|
+
{"version":3,"file":"oauth-user-lookup-B_vnZHKO.mjs","names":[],"sources":["../src/api/handlers/oauth-user-lookup.ts"],"sourcesContent":["/**\n * Shared user lookup for OAuth token operations.\n *\n * Extracts user role and disabled status from the database. Used by\n * handleTokenRefresh() to revalidate scopes against the user's current\n * role and reject disabled users.\n */\n\nimport { toRoleLevel, type RoleLevel } from \"@emdash-cms/auth\";\nimport type { Kysely } from \"kysely\";\n\nimport type { Database } from \"../../database/types.js\";\n\nexport interface UserRoleAndStatus {\n\trole: RoleLevel;\n\tdisabled: boolean;\n}\n\n/**\n * Look up a user's current role and disabled status.\n * Returns null if the user doesn't exist.\n */\nexport async function lookupUserRoleAndStatus(\n\tdb: Kysely<Database>,\n\tuserId: string,\n): Promise<UserRoleAndStatus | null> {\n\tconst row = await db\n\t\t.selectFrom(\"users\")\n\t\t.select([\"role\", \"disabled\"])\n\t\t.where(\"id\", \"=\", userId)\n\t\t.executeTakeFirst();\n\n\tif (!row) return null;\n\n\treturn {\n\t\trole: toRoleLevel(row.role),\n\t\tdisabled: row.disabled === 1,\n\t};\n}\n"],"mappings":";;;;;;;;;;;;;;AAsBA,eAAsB,wBACrB,IACA,QACoC;CACpC,MAAM,MAAM,MAAM,GAChB,WAAW,QAAQ,CACnB,OAAO,CAAC,QAAQ,WAAW,CAAC,CAC5B,MAAM,MAAM,KAAK,OAAO,CACxB,kBAAkB;AAEpB,KAAI,CAAC,IAAK,QAAO;AAEjB,QAAO;EACN,MAAM,YAAY,IAAI,KAAK;EAC3B,UAAU,IAAI,aAAa;EAC3B"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { i as ContentItem } from "./types-
|
|
2
|
-
import { t as Database } from "./types-
|
|
1
|
+
import { i as ContentItem } from "./types-DTniiNto.mjs";
|
|
2
|
+
import { t as Database } from "./types-BPzXTV9x.mjs";
|
|
3
3
|
import { Kysely } from "kysely";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
|
|
@@ -204,4 +204,4 @@ declare class OptionsRepository {
|
|
|
204
204
|
}
|
|
205
205
|
//#endregion
|
|
206
206
|
export { parseQuery as a, handleError as c, ContentListResponse as d, ContentResponse as f, ManifestResponse as h, parseBody as i, ApiContext as l, ListResponse as m, ParseResult as n, apiError as o, FieldDescriptor as p, isParseError as r, apiSuccess as s, OptionsRepository as t, ApiResult as u };
|
|
207
|
-
//# sourceMappingURL=options-
|
|
207
|
+
//# sourceMappingURL=options-DyYIYpPd.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"options-
|
|
1
|
+
{"version":3,"file":"options-DyYIYpPd.d.mts","names":[],"sources":["../src/api/types.ts","../src/api/error.ts","../src/api/parse.ts","../src/database/repositories/options.ts"],"mappings":";;;;;;;;AASA;UAAiB,YAAA;EAChB,KAAA,EAAO,CAAA;EACP,UAAA;EAF6B;;;;;EAQ7B,KAAA;AAAA;AAMD;;;AAAA,UAAiB,mBAAA,SAA4B,YAAA,CAAa,WAAA;AAAA,UAEzC,eAAA;EAChB,IAAA,EAAM,WAAA;;EAEN,IAAA;AAAA;;;;UAMgB,gBAAA;EAChB,OAAA;EACA,IAAA;EACA,WAAA,EAAa,MAAA;IAGX,KAAA;IACA,aAAA;IACA,QAAA;IACA,MAAA,EAAQ,MAAA,SAAe,eAAA;EAAA;EAGzB,OAAA,EAAS,MAAA;IAGP,UAAA,GAAa,KAAA;MAAQ,IAAA;MAAc,SAAA;IAAA;IACnC,OAAA;EAAA;AAAA;AAAA,UAKc,eAAA;EAChB,IAAA;EACA,KAAA;EACA,QAAA;EAZA;;;;EAiBA,OAAA,GAAU,KAAA;IAAQ,KAAA;IAAe,KAAA;EAAA,KAAmB,MAAA;AAAA;AARrD;;;;;;;;;;;;AAAA,KAuBY,SAAA;EACP,OAAA;EAAe,IAAA,EAAM,CAAA;AAAA;EAEvB,OAAA;EACA,KAAA;IAAS,IAAA,EAAM,CAAA;IAAG,OAAA;IAAiB,OAAA,GAAU,MAAA;EAAA;AAAA;;;;UAM/B,UAAA;EAChB,MAAA;EACA,QAAA;AAAA;;;;;;;;;iBC5De,QAAA,CACf,IAAA,UACA,OAAA,UACA,MAAA,UACA,OAAA,GAAU,MAAA,oBACR,QAAA;;;ADZH;;;;iBC2BgB,UAAA,GAAA,CAAc,IAAA,EAAM,CAAA,EAAG,MAAA,YAAe,QAAA;ADzBtD;;;;;;;;AAAA,iBCqCgB,WAAA,CACf,KAAA,WACA,eAAA,UACA,YAAA,WACE,QAAA;;;;;;;KChDS,WAAA,MAAiB,CAAA,GAAI,QAAA;;;;;;AFKjC;iBEGsB,SAAA,WAAoB,CAAA,CAAE,OAAA,CAAA,CAC3C,OAAA,EAAS,OAAA,EACT,MAAA,EAAQ,CAAA,GACN,OAAA,CAAQ,WAAA,CAAY,CAAA,CAAE,KAAA,CAAM,CAAA;;;;;;AFK/B;;iBEyDgB,UAAA,WAAqB,CAAA,CAAE,OAAA,CAAA,CAAS,GAAA,EAAK,GAAA,EAAK,MAAA,EAAQ,CAAA,GAAI,WAAA,CAAY,CAAA,CAAE,KAAA,CAAM,CAAA;;;;;iBA6C1E,YAAA,GAAA,CAAgB,MAAA,EAAQ,WAAA,CAAY,CAAA,IAAK,MAAA,IAAU,QAAA;;;;;;;AF/HnE;;cGKa,iBAAA;EAAA,QACQ,EAAA;cAAA,EAAA,EAAI,MAAA,CAAO,QAAA;EHL/B;;;EGUM,GAAA,aAAA,CAAiB,IAAA,WAAe,OAAA,CAAQ,CAAA;EHHzC;;AAMN;EGYO,YAAA,GAAA,CAAgB,IAAA,UAAc,YAAA,EAAc,CAAA,GAAI,OAAA,CAAQ,CAAA;;;;EAQxD,GAAA,aAAA,CAAiB,IAAA,UAAc,KAAA,EAAO,CAAA,GAAI,OAAA;EHlBjB;;;;;;;;EGwCzB,WAAA,aAAA,CAAyB,IAAA,UAAc,KAAA,EAAO,CAAA,GAAI,OAAA;EH/BxB;;;EGmD1B,MAAA,CAAO,IAAA,WAAe,OAAA;EHhDf;;;EGyDP,MAAA,CAAO,IAAA,WAAe,OAAA;EHhDb;;;EG6DT,OAAA,aAAA,CAAqB,KAAA,aAAkB,OAAA,CAAQ,GAAA,SAAY,CAAA;EHtEpD;;;EG0FP,OAAA,aAAA,CAAqB,OAAA,EAAS,MAAA,SAAe,CAAA,IAAK,OAAA;EHpFtD;;;EGgGI,MAAA,CAAA,GAAU,OAAA,CAAQ,GAAA;EH7Ff;;;EG0GH,WAAA,aAAA,CAAyB,MAAA,WAAiB,OAAA,CAAQ,GAAA,SAAY,CAAA;EHvG/B;;;EG0H/B,cAAA,CAAe,MAAA,WAAiB,OAAA;AAAA"}
|
package/dist/page/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { J as PageFragmentContribution, Z as PageMetadataContribution, et as PageMetadataLinkRel, ht as PublicPageContext, t as BreadcrumbItem, tt as PagePlacement } from "../types-
|
|
2
|
-
import { n as SeoSettings } from "../types-
|
|
1
|
+
import { J as PageFragmentContribution, Z as PageMetadataContribution, et as PageMetadataLinkRel, ht as PublicPageContext, t as BreadcrumbItem, tt as PagePlacement } from "../types-CPAPl93j.mjs";
|
|
2
|
+
import { n as SeoSettings } from "../types-S15DXXNi.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/page/context.d.ts
|
|
5
5
|
/** Fields shared by both input forms */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"passkey-config-
|
|
1
|
+
{"version":3,"file":"passkey-config-C3QgnQnU.mjs","names":[],"sources":["../src/auth/passkey-config.ts"],"sourcesContent":["/**\n * Passkey configuration helper\n *\n * Extracts passkey configuration from the request URL.\n * This ensures the rpId and origin are correctly set for both\n * localhost development and production deployments.\n */\n\nexport interface PasskeyConfig {\n\trpName: string;\n\trpId: string;\n\t/**\n\t * Accepted client-data origins. First entry is the canonical/preferred origin;\n\t * additional entries support multi-origin deployments (e.g. apex + preview\n\t * subdomain sharing the same `rpId`). See `allowedOrigins` parameter.\n\t */\n\torigins: string[];\n}\n\n/**\n * Get passkey configuration from request URL\n *\n * @param url The request URL (typically `new URL(Astro.request.url)` or `new URL(request.url)`)\n * @param siteName Optional site name for rpName (defaults to hostname from `url` or public origin)\n * @param siteUrl Optional browser-facing origin (see `EmDashConfig.siteUrl`).\n * When set, the canonical **origin** and **rpId** are taken from this URL.\n * @param allowedOrigins Optional list of additional accepted origins for verification.\n * Each must share `rpId` with the canonical origin (WebAuthn requirement).\n * Typical use: apex + preview subdomain on the same registrable domain.\n * @throws If `siteUrl` is non-empty but not parseable by `new URL()`.\n */\nexport function getPasskeyConfig(\n\turl: URL,\n\tsiteName?: string,\n\tsiteUrl?: string,\n\tallowedOrigins?: string[],\n): PasskeyConfig {\n\tlet rpName: string;\n\tlet rpId: string;\n\tlet canonicalOrigin: string;\n\n\tif (siteUrl) {\n\t\tlet publicUrl: URL;\n\t\ttry {\n\t\t\tpublicUrl = new URL(siteUrl);\n\t\t} catch (e) {\n\t\t\tthrow new Error(`Invalid siteUrl: \"${siteUrl}\"`, { cause: e });\n\t\t}\n\t\trpName = siteName || publicUrl.hostname;\n\t\trpId = publicUrl.hostname;\n\t\tcanonicalOrigin = publicUrl.origin;\n\t} else {\n\t\trpName = siteName || url.hostname;\n\t\trpId = url.hostname;\n\t\tcanonicalOrigin = url.origin;\n\t}\n\n\tconst origins = [canonicalOrigin];\n\tif (allowedOrigins) {\n\t\tfor (const extra of allowedOrigins) {\n\t\t\tif (extra && !origins.includes(extra)) origins.push(extra);\n\t\t}\n\t}\n\n\treturn { rpName, rpId, origins };\n}\n"],"mappings":";;;;;;;;;;;;;AA+BA,SAAgB,iBACf,KACA,UACA,SACA,gBACgB;CAChB,IAAI;CACJ,IAAI;CACJ,IAAI;AAEJ,KAAI,SAAS;EACZ,IAAI;AACJ,MAAI;AACH,eAAY,IAAI,IAAI,QAAQ;WACpB,GAAG;AACX,SAAM,IAAI,MAAM,qBAAqB,QAAQ,IAAI,EAAE,OAAO,GAAG,CAAC;;AAE/D,WAAS,YAAY,UAAU;AAC/B,SAAO,UAAU;AACjB,oBAAkB,UAAU;QACtB;AACN,WAAS,YAAY,IAAI;AACzB,SAAO,IAAI;AACX,oBAAkB,IAAI;;CAGvB,MAAM,UAAU,CAAC,gBAAgB;AACjC,KAAI,gBACH;OAAK,MAAM,SAAS,eACnB,KAAI,SAAS,CAAC,QAAQ,SAAS,MAAM,CAAE,SAAQ,KAAK,MAAM;;AAI5D,QAAO;EAAE;EAAQ;EAAM;EAAS"}
|
|
@@ -282,4 +282,4 @@ declare function generatePlaceholder(buffer: Uint8Array, mimeType: string, dimen
|
|
|
282
282
|
}): Promise<PlaceholderData | null>;
|
|
283
283
|
//#endregion
|
|
284
284
|
export { MediaValue as _, ComponentEmbed as a, mediaItemToValue as b, EmbedResult as c, MediaListResult as d, MediaProvider as f, MediaUploadInput as g, MediaProviderItem as h, AudioEmbed as i, ImageEmbed as l, MediaProviderDescriptor as m, generatePlaceholder as n, CreateMediaProviderFn as o, MediaProviderCapabilities as p, normalizeMediaValue as r, EmbedOptions as s, PlaceholderData as t, MediaListOptions as u, ThumbnailOptions as v, VideoEmbed as y };
|
|
285
|
-
//# sourceMappingURL=placeholder-
|
|
285
|
+
//# sourceMappingURL=placeholder-CVBv5z8k.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"placeholder-
|
|
1
|
+
{"version":3,"file":"placeholder-CVBv5z8k.d.mts","names":[],"sources":["../src/media/types.ts","../src/media/normalize.ts","../src/media/placeholder.ts"],"mappings":"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"}
|
package/dist/plugin-types.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as PageMetadataHandler, A as EmailDeliverEvent, C as CronHandler, D as EmailAfterSendHandler, E as EmailAfterSendEvent, Et as UninstallHandler, H as MediaAfterUploadEvent, K as MediaUploadEvent, O as EmailBeforeSendEvent, Q as PageMetadataEvent, R as LifecycleEvent, S as CronEvent, Tt as UninstallEvent, U as MediaAfterUploadHandler, W as MediaBeforeUploadHandler, X as PageFragmentHandler, Y as PageFragmentEvent, _ as ContentBeforeDeleteHandler, a as CommentAfterCreateHandler, b as ContentHookEvent, c as CommentBeforeCreateEvent, d as CommentModerateHandler, g as ContentAfterUnpublishHandler, h as ContentAfterSaveHandler, i as CommentAfterCreateEvent, j as EmailDeliverHandler, k as EmailBeforeSendHandler, l as CommentBeforeCreateHandler, m as ContentAfterPublishHandler, o as CommentAfterModerateEvent, p as ContentAfterDeleteHandler, s as CommentAfterModerateHandler, st as PluginContext, u as CommentModerateEvent, v as ContentBeforeSaveHandler, x as ContentPublishStateChangeEvent, y as ContentDeleteEvent, z as LifecycleHandler } from "./types-
|
|
1
|
+
import { $ as PageMetadataHandler, A as EmailDeliverEvent, C as CronHandler, D as EmailAfterSendHandler, E as EmailAfterSendEvent, Et as UninstallHandler, H as MediaAfterUploadEvent, K as MediaUploadEvent, O as EmailBeforeSendEvent, Q as PageMetadataEvent, R as LifecycleEvent, S as CronEvent, Tt as UninstallEvent, U as MediaAfterUploadHandler, W as MediaBeforeUploadHandler, X as PageFragmentHandler, Y as PageFragmentEvent, _ as ContentBeforeDeleteHandler, a as CommentAfterCreateHandler, b as ContentHookEvent, c as CommentBeforeCreateEvent, d as CommentModerateHandler, g as ContentAfterUnpublishHandler, h as ContentAfterSaveHandler, i as CommentAfterCreateEvent, j as EmailDeliverHandler, k as EmailBeforeSendHandler, l as CommentBeforeCreateHandler, m as ContentAfterPublishHandler, o as CommentAfterModerateEvent, p as ContentAfterDeleteHandler, s as CommentAfterModerateHandler, st as PluginContext, u as CommentModerateEvent, v as ContentBeforeSaveHandler, x as ContentPublishStateChangeEvent, y as ContentDeleteEvent, z as LifecycleHandler } from "./types-CPAPl93j.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/plugin-types.d.ts
|
|
4
4
|
/**
|
package/dist/plugin-utils.d.mts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import "./options-
|
|
2
|
-
import "./types-
|
|
3
|
-
import "./types-
|
|
4
|
-
import "./byline-fields-
|
|
5
|
-
import "./index-
|
|
6
|
-
import "./runner-
|
|
7
|
-
import "./index-
|
|
8
|
-
import "./types-
|
|
9
|
-
import "./validate-
|
|
1
|
+
import "./options-DyYIYpPd.mjs";
|
|
2
|
+
import "./types-BPzXTV9x.mjs";
|
|
3
|
+
import "./types-CPAPl93j.mjs";
|
|
4
|
+
import "./byline-fields-DbibsvTl.mjs";
|
|
5
|
+
import "./index-B1keaX5Y.mjs";
|
|
6
|
+
import "./runner-DTdhuI9i.mjs";
|
|
7
|
+
import "./index-DR56od45.mjs";
|
|
8
|
+
import "./types-BFgrqwSk.mjs";
|
|
9
|
+
import "./validate-CNwkPWzz.mjs";
|
|
10
10
|
import { EmDashHandlers } from "./astro/types.mjs";
|
|
11
11
|
|
|
12
12
|
//#region src/plugin-utils.d.ts
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import "../options-
|
|
2
|
-
import "../types-
|
|
3
|
-
import { yt as ResolvedPlugin } from "../types-
|
|
4
|
-
import "../byline-fields-
|
|
5
|
-
import { Vt as PluginDescriptor } from "../index-
|
|
6
|
-
import "../runner-
|
|
7
|
-
import "../index-
|
|
1
|
+
import "../options-DyYIYpPd.mjs";
|
|
2
|
+
import "../types-BPzXTV9x.mjs";
|
|
3
|
+
import { yt as ResolvedPlugin } from "../types-CPAPl93j.mjs";
|
|
4
|
+
import "../byline-fields-DbibsvTl.mjs";
|
|
5
|
+
import { Vt as PluginDescriptor } from "../index-B1keaX5Y.mjs";
|
|
6
|
+
import "../runner-DTdhuI9i.mjs";
|
|
7
|
+
import "../index-DR56od45.mjs";
|
|
8
8
|
import { SandboxedPlugin } from "../plugin-types.mjs";
|
|
9
|
-
import "../types-
|
|
10
|
-
import "../validate-
|
|
9
|
+
import "../types-BFgrqwSk.mjs";
|
|
10
|
+
import "../validate-CNwkPWzz.mjs";
|
|
11
11
|
|
|
12
12
|
//#region src/plugins/adapt-sandbox-entry.d.ts
|
|
13
13
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { n as PLUGIN_CAPABILITIES, t as HOOK_NAMES } from "../manifest-schema-
|
|
2
|
-
import {
|
|
1
|
+
import { n as PLUGIN_CAPABILITIES, t as HOOK_NAMES } from "../manifest-schema-BtwbL_vj.mjs";
|
|
2
|
+
import { a as normalizeCapabilities } from "../types-CZI4E3qG.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/plugins/adapt-sandbox-entry.ts
|
|
5
5
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"public-url-
|
|
1
|
+
{"version":3,"file":"public-url-BFVC2OTJ.mjs","names":[],"sources":["../src/api/public-url.ts"],"sourcesContent":["/**\n * Public URL helpers for reverse-proxy deployments.\n *\n * Behind a TLS-terminating proxy the internal request URL\n * (`http://localhost:4321`) differs from the browser-facing origin\n * (`https://mysite.example.com`). These pure helpers resolve the\n * correct public origin from config, falling back to the request URL.\n *\n * Workers-safe: no Node.js imports.\n */\n\n/** Minimal config shape — avoids importing the full EmDashConfig type tree. */\ninterface SiteUrlConfig {\n\tsiteUrl?: string;\n}\n\n/**\n * Resolve siteUrl from runtime environment variables.\n *\n * Uses process.env (not import.meta.env) because Vite statically replaces\n * import.meta.env at build time, baking out any env vars not present during\n * the build. Container deployments set env vars at runtime, so we must read\n * process.env which Vite leaves untouched.\n *\n * On Cloudflare Workers process.env is unavailable (returns undefined),\n * so the fallback chain continues to url.origin.\n *\n * Caches after first call.\n */\nlet _envSiteUrl: string | undefined | null = null;\n\n/** @internal Reset cached env values — test-only. */\nexport function _resetEnvCache(): void {\n\t_envSiteUrl = null;\n\t_envAllowedOrigins = null;\n}\n\nfunction getEnvSiteUrl(): string | undefined {\n\tif (_envSiteUrl !== null) return _envSiteUrl || undefined;\n\ttry {\n\t\t// process.env is available on Node.js; undefined on Workers\n\t\tconst value =\n\t\t\t(typeof process !== \"undefined\" && process.env?.EMDASH_SITE_URL) ||\n\t\t\t(typeof process !== \"undefined\" && process.env?.SITE_URL) ||\n\t\t\t\"\";\n\t\tif (value) {\n\t\t\tconst parsed = new URL(value);\n\t\t\tif (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n\t\t\t\t_envSiteUrl = \"\";\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\t_envSiteUrl = parsed.origin;\n\t\t} else {\n\t\t\t_envSiteUrl = \"\";\n\t\t}\n\t} catch {\n\t\t_envSiteUrl = \"\";\n\t}\n\treturn _envSiteUrl || undefined;\n}\n\n/**\n * Return the public-facing origin for the site.\n *\n * Resolution order:\n * 1. `config.siteUrl` (set in astro.config.mjs, origin-normalized at startup)\n * 2. `EMDASH_SITE_URL` or `SITE_URL` env var (resolved at runtime for containers)\n * 3. `url.origin` (internal request URL — correct when no proxy)\n *\n * @param url The request URL (`new URL(request.url)` or `Astro.url`)\n * @param config The EmDash config (from `locals.emdash?.config`)\n * @returns Origin string, e.g. `\"https://mysite.example.com\"`\n */\nexport function getPublicOrigin(url: URL, config?: SiteUrlConfig): string {\n\treturn config?.siteUrl || getEnvSiteUrl() || url.origin;\n}\n\n/**\n * Resolve additional accepted passkey origins from runtime environment.\n *\n * Reads `EMDASH_ALLOWED_ORIGINS` (comma-separated list of origins) for\n * multi-origin deployments where the same RP is reachable under several\n * hostnames sharing the registrable parent domain (e.g. apex + preview).\n *\n * Each entry is parsed via `new URL()` and reduced to its `origin`. Unlike\n * `getEnvSiteUrl` (which silently falls back to `url.origin` on bad input),\n * this throws on any unparseable or non-http(s) entry — `EMDASH_ALLOWED_ORIGINS`\n * is an allowlist for passkey verification, so silently dropping a typo would\n * surface as \"I can't authenticate on this origin\" with no diagnostic. Fail\n * loud at first read.\n *\n * Uses `process.env` (Vite leaves it untouched at runtime). Result is cached\n * on success.\n */\nlet _envAllowedOrigins: string[] | null = null;\n\nexport function getEnvAllowedOrigins(): string[] {\n\tif (_envAllowedOrigins !== null) return _envAllowedOrigins;\n\tconst raw = typeof process !== \"undefined\" ? process.env?.EMDASH_ALLOWED_ORIGINS || \"\" : \"\";\n\tconst parsed: string[] = [];\n\tfor (const entry of raw.split(\",\")) {\n\t\tconst trimmed = entry.trim();\n\t\tif (!trimmed) continue;\n\t\tlet u: URL;\n\t\ttry {\n\t\t\tu = new URL(trimmed);\n\t\t} catch (e) {\n\t\t\tthrow new Error(`EmDash config error in EMDASH_ALLOWED_ORIGINS: invalid URL: \"${trimmed}\"`, {\n\t\t\t\tcause: e,\n\t\t\t});\n\t\t}\n\t\tif (u.protocol !== \"http:\" && u.protocol !== \"https:\") {\n\t\t\tthrow new Error(\n\t\t\t\t`EmDash config error in EMDASH_ALLOWED_ORIGINS: origin must be http or https: \"${trimmed}\" (got ${u.protocol})`,\n\t\t\t);\n\t\t}\n\t\tparsed.push(u.origin);\n\t}\n\t_envAllowedOrigins = parsed;\n\treturn parsed;\n}\n\n/**\n * Build a full public URL by appending a path to the public origin.\n *\n * @param url The request URL\n * @param config The EmDash config\n * @param path Path to append (must start with `/`)\n * @returns Full URL string, e.g. `\"https://mysite.example.com/_emdash/admin/login\"`\n */\nexport function getPublicUrl(url: URL, config: SiteUrlConfig | undefined, path: string): string {\n\treturn `${getPublicOrigin(url, config)}${path}`;\n}\n"],"mappings":";;;;;;;;;;;;;;AA6BA,IAAI,cAAyC;AAQ7C,SAAS,gBAAoC;AAC5C,KAAI,gBAAgB,KAAM,QAAO,eAAe;AAChD,KAAI;EAEH,MAAM,QACJ,OAAO,YAAY,eAAe,QAAQ,KAAK,mBAC/C,OAAO,YAAY,eAAe,QAAQ,KAAK,YAChD;AACD,MAAI,OAAO;GACV,MAAM,SAAS,IAAI,IAAI,MAAM;AAC7B,OAAI,OAAO,aAAa,WAAW,OAAO,aAAa,UAAU;AAChE,kBAAc;AACd;;AAED,iBAAc,OAAO;QAErB,eAAc;SAER;AACP,gBAAc;;AAEf,QAAO,eAAe;;;;;;;;;;;;;;AAevB,SAAgB,gBAAgB,KAAU,QAAgC;AACzE,QAAO,QAAQ,WAAW,eAAe,IAAI,IAAI;;;;;;;;;;;;;;;;;;;AAoBlD,IAAI,qBAAsC;AAE1C,SAAgB,uBAAiC;AAChD,KAAI,uBAAuB,KAAM,QAAO;CACxC,MAAM,MAAM,OAAO,YAAY,cAAc,QAAQ,KAAK,0BAA0B,KAAK;CACzF,MAAM,SAAmB,EAAE;AAC3B,MAAK,MAAM,SAAS,IAAI,MAAM,IAAI,EAAE;EACnC,MAAM,UAAU,MAAM,MAAM;AAC5B,MAAI,CAAC,QAAS;EACd,IAAI;AACJ,MAAI;AACH,OAAI,IAAI,IAAI,QAAQ;WACZ,GAAG;AACX,SAAM,IAAI,MAAM,gEAAgE,QAAQ,IAAI,EAC3F,OAAO,GACP,CAAC;;AAEH,MAAI,EAAE,aAAa,WAAW,EAAE,aAAa,SAC5C,OAAM,IAAI,MACT,iFAAiF,QAAQ,SAAS,EAAE,SAAS,GAC7G;AAEF,SAAO,KAAK,EAAE,OAAO;;AAEtB,sBAAqB;AACrB,QAAO"}
|
|
@@ -4,7 +4,7 @@ import { a as encodeCursor } from "./types-BXSUSAjt.mjs";
|
|
|
4
4
|
import { getRequestContext } from "./request-context.mjs";
|
|
5
5
|
import { r as requestCached } from "./request-cache-D32LpnmI.mjs";
|
|
6
6
|
import { n as isMissingTableError } from "./db-errors-CtzxKBxe.mjs";
|
|
7
|
-
import { t as CURSOR_RAW_VALUES } from "./loader-
|
|
7
|
+
import { t as CURSOR_RAW_VALUES } from "./loader-ZN1ll-d-.mjs";
|
|
8
8
|
|
|
9
9
|
//#region src/visual-editing/editable.ts
|
|
10
10
|
/**
|
|
@@ -137,6 +137,14 @@ function dataStr(data, key, fallback = "") {
|
|
|
137
137
|
const val = data[key];
|
|
138
138
|
return typeof val === "string" ? val : fallback;
|
|
139
139
|
}
|
|
140
|
+
/** Safely read a date-like field from a Record */
|
|
141
|
+
function dataDate(data, key) {
|
|
142
|
+
const val = data[key];
|
|
143
|
+
if (val instanceof Date) return Number.isNaN(val.getTime()) ? void 0 : val;
|
|
144
|
+
if (typeof val !== "string" && typeof val !== "number") return void 0;
|
|
145
|
+
const date = new Date(val);
|
|
146
|
+
return Number.isNaN(date.getTime()) ? void 0 : date;
|
|
147
|
+
}
|
|
140
148
|
/** Type guard for Record<string, unknown> */
|
|
141
149
|
function isRecord(value) {
|
|
142
150
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -386,8 +394,10 @@ async function getEmDashEntry(type, id, options) {
|
|
|
386
394
|
function isVisible(entry) {
|
|
387
395
|
const data = entryData(entry);
|
|
388
396
|
const status = dataStr(data, "status");
|
|
389
|
-
const scheduledAt =
|
|
390
|
-
|
|
397
|
+
const scheduledAt = dataDate(data, "scheduledAt");
|
|
398
|
+
const isPublished = status === "published";
|
|
399
|
+
const isScheduledAndReady = status === "scheduled" && scheduledAt !== void 0 && scheduledAt.getTime() <= Date.now();
|
|
400
|
+
return isPublished || !!isScheduledAndReady;
|
|
391
401
|
}
|
|
392
402
|
const localeChain = requestedLocale && isI18nEnabled() ? getFallbackChain(requestedLocale) : [requestedLocale];
|
|
393
403
|
/** Return a successful EntryResult with bylines and taxonomy terms hydrated */
|
|
@@ -492,7 +502,7 @@ async function getEmDashEntry(type, id, options) {
|
|
|
492
502
|
async function hydrateEntryBylines(type, entries) {
|
|
493
503
|
if (entries.length === 0) return;
|
|
494
504
|
try {
|
|
495
|
-
const { getBylinesForEntries } = await import("./bylines-
|
|
505
|
+
const { getBylinesForEntries } = await import("./bylines-s8c2DXbH.mjs").then((n) => n.t);
|
|
496
506
|
const refs = entries.map((e) => {
|
|
497
507
|
const data = entryData(e);
|
|
498
508
|
const id = dataStr(data, "id");
|
|
@@ -543,7 +553,7 @@ async function hydrateEntryBylines(type, entries) {
|
|
|
543
553
|
async function hydrateEntryTerms(type, entries, locale) {
|
|
544
554
|
if (entries.length === 0) return;
|
|
545
555
|
try {
|
|
546
|
-
const { getAllTermsForEntries } = await import("./taxonomies-
|
|
556
|
+
const { getAllTermsForEntries } = await import("./taxonomies-BdAmbOwx.mjs").then((n) => n.u);
|
|
547
557
|
const ids = entries.map((e) => dataStr(entryData(e), "id")).filter(Boolean);
|
|
548
558
|
if (ids.length === 0) return;
|
|
549
559
|
const termsMap = await getAllTermsForEntries(type, ids, { locale });
|
|
@@ -577,7 +587,7 @@ async function hydrateEntryTerms(type, entries, locale) {
|
|
|
577
587
|
*/
|
|
578
588
|
async function getTranslations(type, id) {
|
|
579
589
|
try {
|
|
580
|
-
const db = (await import("./loader-
|
|
590
|
+
const db = (await import("./loader-ZN1ll-d-.mjs").then((n) => n.i)).getDb;
|
|
581
591
|
const dbInstance = await db();
|
|
582
592
|
const { ContentRepository } = await import("./content-BIlVx-RX.mjs").then((n) => n.n);
|
|
583
593
|
const repo = new ContentRepository(dbInstance);
|
|
@@ -648,7 +658,7 @@ function invalidateUrlPatternCache() {
|
|
|
648
658
|
*/
|
|
649
659
|
async function resolveEmDashPath(path) {
|
|
650
660
|
if (!cachedUrlPatterns) {
|
|
651
|
-
const { getDb } = await import("./loader-
|
|
661
|
+
const { getDb } = await import("./loader-ZN1ll-d-.mjs").then((n) => n.i);
|
|
652
662
|
const { SchemaRegistry } = await import("./registry-brYh-rAT.mjs").then((n) => n.r);
|
|
653
663
|
const collections = await new SchemaRegistry(await getDb()).listCollections();
|
|
654
664
|
cachedUrlPatterns = [];
|
|
@@ -681,4 +691,4 @@ async function resolveEmDashPath(path) {
|
|
|
681
691
|
|
|
682
692
|
//#endregion
|
|
683
693
|
export { invalidateUrlPatternCache as a, createEditable as c, getTranslations as i, createNoop as l, getEmDashCollection as n, query_exports as o, getEmDashEntry as r, resolveEmDashPath as s, getEditMeta as t };
|
|
684
|
-
//# sourceMappingURL=query-
|
|
694
|
+
//# sourceMappingURL=query-CbUcI4Xk.mjs.map
|