emdash 0.0.0-b → 0.0.2
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/README.md +87 -43
- package/dist/adapters-BLMa4JGD.d.mts +106 -0
- package/dist/adapters-BLMa4JGD.d.mts.map +1 -0
- package/dist/apply-Bjfq_b4-.mjs +1293 -0
- package/dist/apply-Bjfq_b4-.mjs.map +1 -0
- package/dist/astro/index.d.mts +51 -0
- package/dist/astro/index.d.mts.map +1 -0
- package/dist/astro/index.mjs +1336 -0
- package/dist/astro/index.mjs.map +1 -0
- package/dist/astro/middleware/auth.d.mts +31 -0
- package/dist/astro/middleware/auth.d.mts.map +1 -0
- package/dist/astro/middleware/auth.mjs +654 -0
- package/dist/astro/middleware/auth.mjs.map +1 -0
- package/dist/astro/middleware/redirect.d.mts +22 -0
- package/dist/astro/middleware/redirect.d.mts.map +1 -0
- package/dist/astro/middleware/redirect.mjs +63 -0
- package/dist/astro/middleware/redirect.mjs.map +1 -0
- package/dist/astro/middleware/request-context.d.mts +18 -0
- package/dist/astro/middleware/request-context.d.mts.map +1 -0
- package/dist/astro/middleware/request-context.mjs +1310 -0
- package/dist/astro/middleware/request-context.mjs.map +1 -0
- package/dist/astro/middleware/setup.d.mts +20 -0
- package/dist/astro/middleware/setup.d.mts.map +1 -0
- package/dist/astro/middleware/setup.mjs +47 -0
- package/dist/astro/middleware/setup.mjs.map +1 -0
- package/dist/astro/middleware.d.mts +13 -0
- package/dist/astro/middleware.d.mts.map +1 -0
- package/dist/astro/middleware.mjs +1613 -0
- package/dist/astro/middleware.mjs.map +1 -0
- package/dist/astro/types.d.mts +250 -0
- package/dist/astro/types.d.mts.map +1 -0
- package/dist/astro/types.mjs +1 -0
- package/dist/base64-MBPo9ozB.mjs +59 -0
- package/dist/base64-MBPo9ozB.mjs.map +1 -0
- package/dist/byline-CL847F26.mjs +213 -0
- package/dist/byline-CL847F26.mjs.map +1 -0
- package/dist/bylines-C2a-2TGt.mjs +136 -0
- package/dist/bylines-C2a-2TGt.mjs.map +1 -0
- package/dist/chunk-ClPoSABd.mjs +21 -0
- package/dist/cli/index.d.mts +1 -0
- package/dist/cli/index.mjs +3909 -0
- package/dist/cli/index.mjs.map +1 -0
- package/dist/client/cf-access.d.mts +60 -0
- package/dist/client/cf-access.d.mts.map +1 -0
- package/dist/client/cf-access.mjs +179 -0
- package/dist/client/cf-access.mjs.map +1 -0
- package/dist/client/index.d.mts +398 -0
- package/dist/client/index.d.mts.map +1 -0
- package/dist/client/index.mjs +346 -0
- package/dist/client/index.mjs.map +1 -0
- package/dist/config-CKE8p9xM.mjs +55 -0
- package/dist/config-CKE8p9xM.mjs.map +1 -0
- package/dist/connection-B4zVnQIa.mjs +40 -0
- package/dist/connection-B4zVnQIa.mjs.map +1 -0
- package/dist/content-D6C2WsZC.mjs +824 -0
- package/dist/content-D6C2WsZC.mjs.map +1 -0
- package/dist/db/index.d.mts +4 -0
- package/dist/db/index.mjs +62 -0
- package/dist/db/index.mjs.map +1 -0
- package/dist/db/libsql.d.mts +11 -0
- package/dist/db/libsql.d.mts.map +1 -0
- package/dist/db/libsql.mjs +17 -0
- package/dist/db/libsql.mjs.map +1 -0
- package/dist/db/postgres.d.mts +11 -0
- package/dist/db/postgres.d.mts.map +1 -0
- package/dist/db/postgres.mjs +30 -0
- package/dist/db/postgres.mjs.map +1 -0
- package/dist/db/sqlite.d.mts +11 -0
- package/dist/db/sqlite.d.mts.map +1 -0
- package/dist/db/sqlite.mjs +16 -0
- package/dist/db/sqlite.mjs.map +1 -0
- package/dist/default-Cyi4aAxu.mjs +81 -0
- package/dist/default-Cyi4aAxu.mjs.map +1 -0
- package/dist/dialect-helpers-B9uSp2GJ.mjs +90 -0
- package/dist/dialect-helpers-B9uSp2GJ.mjs.map +1 -0
- package/dist/error-Cxz0tQeO.mjs +27 -0
- package/dist/error-Cxz0tQeO.mjs.map +1 -0
- package/dist/index-C1xF3OGh.d.mts +4527 -0
- package/dist/index-C1xF3OGh.d.mts.map +1 -0
- package/dist/index.d.mts +16 -0
- package/dist/index.mjs +30 -0
- package/dist/load-yOOlckBj.mjs +28 -0
- package/dist/load-yOOlckBj.mjs.map +1 -0
- package/dist/loader-fz8Q_3EO.mjs +447 -0
- package/dist/loader-fz8Q_3EO.mjs.map +1 -0
- package/dist/manifest-schema-Dcl0R6nM.mjs +184 -0
- package/dist/manifest-schema-Dcl0R6nM.mjs.map +1 -0
- package/dist/media/index.d.mts +26 -0
- package/dist/media/index.d.mts.map +1 -0
- package/dist/media/index.mjs +55 -0
- package/dist/media/index.mjs.map +1 -0
- package/dist/media/local-runtime.d.mts +39 -0
- package/dist/media/local-runtime.d.mts.map +1 -0
- package/dist/media/local-runtime.mjs +133 -0
- package/dist/media/local-runtime.mjs.map +1 -0
- package/dist/media-DqHVh136.mjs +200 -0
- package/dist/media-DqHVh136.mjs.map +1 -0
- package/dist/mode-C2EzN1uE.mjs +23 -0
- package/dist/mode-C2EzN1uE.mjs.map +1 -0
- package/dist/page/index.d.mts +140 -0
- package/dist/page/index.d.mts.map +1 -0
- package/dist/page/index.mjs +416 -0
- package/dist/page/index.mjs.map +1 -0
- package/dist/placeholder-CmGAmqeO.d.mts +276 -0
- package/dist/placeholder-CmGAmqeO.d.mts.map +1 -0
- package/dist/placeholder-SmpOx-_v.mjs +243 -0
- package/dist/placeholder-SmpOx-_v.mjs.map +1 -0
- package/dist/plugin-utils.d.mts +58 -0
- package/dist/plugin-utils.d.mts.map +1 -0
- package/dist/plugin-utils.mjs +78 -0
- package/dist/plugin-utils.mjs.map +1 -0
- package/dist/plugins/adapt-sandbox-entry.d.mts +22 -0
- package/dist/plugins/adapt-sandbox-entry.d.mts.map +1 -0
- package/dist/plugins/adapt-sandbox-entry.mjs +113 -0
- package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -0
- package/dist/query-CS_iSj34.mjs +460 -0
- package/dist/query-CS_iSj34.mjs.map +1 -0
- package/dist/redirect-DIfIni3r.mjs +329 -0
- package/dist/redirect-DIfIni3r.mjs.map +1 -0
- package/dist/registry-D_w5HW4G.mjs +863 -0
- package/dist/registry-D_w5HW4G.mjs.map +1 -0
- package/dist/request-context.d.mts +49 -0
- package/dist/request-context.d.mts.map +1 -0
- package/dist/request-context.mjs +43 -0
- package/dist/request-context.mjs.map +1 -0
- package/dist/runner-C0hCbYnD.mjs +1412 -0
- package/dist/runner-C0hCbYnD.mjs.map +1 -0
- package/dist/runner-EAtf0ZIe.d.mts +27 -0
- package/dist/runner-EAtf0ZIe.d.mts.map +1 -0
- package/dist/runtime.d.mts +26 -0
- package/dist/runtime.d.mts.map +1 -0
- package/dist/runtime.mjs +42 -0
- package/dist/runtime.mjs.map +1 -0
- package/dist/search-DG603UrT.mjs +9211 -0
- package/dist/search-DG603UrT.mjs.map +1 -0
- package/dist/seed/index.d.mts +3 -0
- package/dist/seed/index.mjs +15 -0
- package/dist/seo/index.d.mts +70 -0
- package/dist/seo/index.d.mts.map +1 -0
- package/dist/seo/index.mjs +70 -0
- package/dist/seo/index.mjs.map +1 -0
- package/dist/storage/local.d.mts +39 -0
- package/dist/storage/local.d.mts.map +1 -0
- package/dist/storage/local.mjs +166 -0
- package/dist/storage/local.mjs.map +1 -0
- package/dist/storage/s3.d.mts +32 -0
- package/dist/storage/s3.d.mts.map +1 -0
- package/dist/storage/s3.mjs +175 -0
- package/dist/storage/s3.mjs.map +1 -0
- package/dist/tokens-DpgrkrXK.mjs +171 -0
- package/dist/tokens-DpgrkrXK.mjs.map +1 -0
- package/dist/transport-BFGblqwG.d.mts +42 -0
- package/dist/transport-BFGblqwG.d.mts.map +1 -0
- package/dist/transport-yxiQsi8I.mjs +418 -0
- package/dist/transport-yxiQsi8I.mjs.map +1 -0
- package/dist/types-BRuPJGdV.d.mts +102 -0
- package/dist/types-BRuPJGdV.d.mts.map +1 -0
- package/dist/types-C4-fAxN3.d.mts +182 -0
- package/dist/types-C4-fAxN3.d.mts.map +1 -0
- package/dist/types-CMMN0pNg.mjs +31 -0
- package/dist/types-CMMN0pNg.mjs.map +1 -0
- package/dist/types-CUBbjgmP.mjs +16 -0
- package/dist/types-CUBbjgmP.mjs.map +1 -0
- package/dist/types-DRjfYOEv.d.mts +426 -0
- package/dist/types-DRjfYOEv.d.mts.map +1 -0
- package/dist/types-DY5zk5HN.mjs +73 -0
- package/dist/types-DY5zk5HN.mjs.map +1 -0
- package/dist/types-DaNLHo_T.d.mts +184 -0
- package/dist/types-DaNLHo_T.d.mts.map +1 -0
- package/dist/types-DvhsUmSJ.d.mts +1111 -0
- package/dist/types-DvhsUmSJ.d.mts.map +1 -0
- package/dist/validate-CpBtVMsD.d.mts +378 -0
- package/dist/validate-CpBtVMsD.d.mts.map +1 -0
- package/dist/validate-CqRJb_xU.mjs +97 -0
- package/dist/validate-CqRJb_xU.mjs.map +1 -0
- package/dist/validate-O7PWmlnq.mjs +328 -0
- package/dist/validate-O7PWmlnq.mjs.map +1 -0
- package/locals.d.ts +46 -0
- package/package.json +233 -19
- package/src/api/authorize.ts +63 -0
- package/src/api/csrf.ts +48 -0
- package/src/api/error.ts +99 -0
- package/src/api/errors.ts +445 -0
- package/src/api/escape.ts +9 -0
- package/src/api/handlers/api-tokens.ts +240 -0
- package/src/api/handlers/comments.ts +314 -0
- package/src/api/handlers/content.ts +1315 -0
- package/src/api/handlers/dashboard.ts +205 -0
- package/src/api/handlers/device-flow.ts +684 -0
- package/src/api/handlers/index.ts +163 -0
- package/src/api/handlers/manifest.ts +158 -0
- package/src/api/handlers/marketplace.ts +930 -0
- package/src/api/handlers/media.ts +207 -0
- package/src/api/handlers/menus.ts +493 -0
- package/src/api/handlers/oauth-authorization.ts +429 -0
- package/src/api/handlers/oauth-clients.ts +349 -0
- package/src/api/handlers/oauth-user-lookup.ts +39 -0
- package/src/api/handlers/plugins.ts +254 -0
- package/src/api/handlers/redirects.ts +360 -0
- package/src/api/handlers/revision.ts +145 -0
- package/src/api/handlers/schema.ts +534 -0
- package/src/api/handlers/sections.ts +289 -0
- package/src/api/handlers/seo.ts +115 -0
- package/src/api/handlers/settings.ts +49 -0
- package/src/api/handlers/snapshot.ts +350 -0
- package/src/api/handlers/taxonomies.ts +523 -0
- package/src/api/index.ts +6 -0
- package/src/api/openapi/document.ts +2368 -0
- package/src/api/openapi/index.ts +1 -0
- package/src/api/parse.ts +139 -0
- package/src/api/redirect.ts +14 -0
- package/src/api/rev.ts +67 -0
- package/src/api/schemas/auth.ts +112 -0
- package/src/api/schemas/bylines.ts +85 -0
- package/src/api/schemas/comments.ts +117 -0
- package/src/api/schemas/common.ts +89 -0
- package/src/api/schemas/content.ts +191 -0
- package/src/api/schemas/import.ts +52 -0
- package/src/api/schemas/index.ts +17 -0
- package/src/api/schemas/media.ts +116 -0
- package/src/api/schemas/menus.ts +111 -0
- package/src/api/schemas/redirects.ts +155 -0
- package/src/api/schemas/schema.ts +203 -0
- package/src/api/schemas/search.ts +63 -0
- package/src/api/schemas/sections.ts +67 -0
- package/src/api/schemas/settings.ts +63 -0
- package/src/api/schemas/setup.ts +37 -0
- package/src/api/schemas/taxonomies.ts +113 -0
- package/src/api/schemas/users.ts +96 -0
- package/src/api/schemas/widgets.ts +80 -0
- package/src/api/site-url.ts +25 -0
- package/src/api/types.ts +82 -0
- package/src/astro/index.ts +27 -0
- package/src/astro/integration/index.ts +303 -0
- package/src/astro/integration/routes.ts +834 -0
- package/src/astro/integration/runtime.ts +338 -0
- package/src/astro/integration/virtual-modules.ts +469 -0
- package/src/astro/integration/vite-config.ts +335 -0
- package/src/astro/middleware/auth.ts +743 -0
- package/src/astro/middleware/redirect.ts +89 -0
- package/src/astro/middleware/request-context.ts +129 -0
- package/src/astro/middleware/setup.ts +89 -0
- package/src/astro/middleware.ts +398 -0
- package/src/astro/routes/PluginRegistry.tsx +15 -0
- package/src/astro/routes/admin.astro +81 -0
- package/src/astro/routes/api/admin/allowed-domains/[domain].ts +112 -0
- package/src/astro/routes/api/admin/allowed-domains/index.ts +108 -0
- package/src/astro/routes/api/admin/api-tokens/[id].ts +40 -0
- package/src/astro/routes/api/admin/api-tokens/index.ts +68 -0
- package/src/astro/routes/api/admin/bylines/[id]/index.ts +87 -0
- package/src/astro/routes/api/admin/bylines/index.ts +72 -0
- package/src/astro/routes/api/admin/comments/[id]/status.ts +116 -0
- package/src/astro/routes/api/admin/comments/[id].ts +64 -0
- package/src/astro/routes/api/admin/comments/bulk.ts +42 -0
- package/src/astro/routes/api/admin/comments/counts.ts +30 -0
- package/src/astro/routes/api/admin/comments/index.ts +46 -0
- package/src/astro/routes/api/admin/hooks/exclusive/[hookName].ts +91 -0
- package/src/astro/routes/api/admin/hooks/exclusive/index.ts +51 -0
- package/src/astro/routes/api/admin/oauth-clients/[id].ts +110 -0
- package/src/astro/routes/api/admin/oauth-clients/index.ts +71 -0
- package/src/astro/routes/api/admin/plugins/[id]/disable.ts +39 -0
- package/src/astro/routes/api/admin/plugins/[id]/enable.ts +39 -0
- package/src/astro/routes/api/admin/plugins/[id]/index.ts +38 -0
- package/src/astro/routes/api/admin/plugins/[id]/uninstall.ts +48 -0
- package/src/astro/routes/api/admin/plugins/[id]/update.ts +59 -0
- package/src/astro/routes/api/admin/plugins/index.ts +32 -0
- package/src/astro/routes/api/admin/plugins/marketplace/[id]/icon.ts +61 -0
- package/src/astro/routes/api/admin/plugins/marketplace/[id]/index.ts +33 -0
- package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +62 -0
- package/src/astro/routes/api/admin/plugins/marketplace/index.ts +38 -0
- package/src/astro/routes/api/admin/plugins/updates.ts +28 -0
- package/src/astro/routes/api/admin/themes/marketplace/[id]/index.ts +33 -0
- package/src/astro/routes/api/admin/themes/marketplace/[id]/thumbnail.ts +61 -0
- package/src/astro/routes/api/admin/themes/marketplace/index.ts +45 -0
- package/src/astro/routes/api/admin/users/[id]/disable.ts +69 -0
- package/src/astro/routes/api/admin/users/[id]/enable.ts +48 -0
- package/src/astro/routes/api/admin/users/[id]/index.ts +146 -0
- package/src/astro/routes/api/admin/users/[id]/send-recovery.ts +72 -0
- package/src/astro/routes/api/admin/users/index.ts +66 -0
- package/src/astro/routes/api/auth/dev-bypass.ts +139 -0
- package/src/astro/routes/api/auth/invite/accept.ts +52 -0
- package/src/astro/routes/api/auth/invite/complete.ts +84 -0
- package/src/astro/routes/api/auth/invite/index.ts +99 -0
- package/src/astro/routes/api/auth/logout.ts +40 -0
- package/src/astro/routes/api/auth/magic-link/send.ts +89 -0
- package/src/astro/routes/api/auth/magic-link/verify.ts +71 -0
- package/src/astro/routes/api/auth/me.ts +60 -0
- package/src/astro/routes/api/auth/oauth/[provider]/callback.ts +219 -0
- package/src/astro/routes/api/auth/oauth/[provider].ts +119 -0
- package/src/astro/routes/api/auth/passkey/[id].ts +124 -0
- package/src/astro/routes/api/auth/passkey/index.ts +54 -0
- package/src/astro/routes/api/auth/passkey/options.ts +82 -0
- package/src/astro/routes/api/auth/passkey/register/options.ts +86 -0
- package/src/astro/routes/api/auth/passkey/register/verify.ts +115 -0
- package/src/astro/routes/api/auth/passkey/verify.ts +66 -0
- package/src/astro/routes/api/auth/signup/complete.ts +85 -0
- package/src/astro/routes/api/auth/signup/request.ts +77 -0
- package/src/astro/routes/api/auth/signup/verify.ts +53 -0
- package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +312 -0
- package/src/astro/routes/api/content/[collection]/[id]/compare.ts +28 -0
- package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +54 -0
- package/src/astro/routes/api/content/[collection]/[id]/duplicate.ts +61 -0
- package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +33 -0
- package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +107 -0
- package/src/astro/routes/api/content/[collection]/[id]/publish.ts +56 -0
- package/src/astro/routes/api/content/[collection]/[id]/restore.ts +54 -0
- package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +31 -0
- package/src/astro/routes/api/content/[collection]/[id]/schedule.ts +101 -0
- package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +140 -0
- package/src/astro/routes/api/content/[collection]/[id]/translations.ts +30 -0
- package/src/astro/routes/api/content/[collection]/[id]/unpublish.ts +56 -0
- package/src/astro/routes/api/content/[collection]/[id].ts +137 -0
- package/src/astro/routes/api/content/[collection]/index.ts +59 -0
- package/src/astro/routes/api/content/[collection]/trash.ts +33 -0
- package/src/astro/routes/api/dashboard.ts +32 -0
- package/src/astro/routes/api/dev/emails.ts +36 -0
- package/src/astro/routes/api/import/probe.ts +47 -0
- package/src/astro/routes/api/import/wordpress/analyze.ts +510 -0
- package/src/astro/routes/api/import/wordpress/execute.ts +283 -0
- package/src/astro/routes/api/import/wordpress/media.ts +338 -0
- package/src/astro/routes/api/import/wordpress/prepare.ts +181 -0
- package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +393 -0
- package/src/astro/routes/api/import/wordpress-plugin/analyze.ts +111 -0
- package/src/astro/routes/api/import/wordpress-plugin/callback.ts +58 -0
- package/src/astro/routes/api/import/wordpress-plugin/execute.ts +347 -0
- package/src/astro/routes/api/manifest.ts +62 -0
- package/src/astro/routes/api/mcp.ts +124 -0
- package/src/astro/routes/api/media/[id]/confirm.ts +93 -0
- package/src/astro/routes/api/media/[id].ts +145 -0
- package/src/astro/routes/api/media/file/[key].ts +79 -0
- package/src/astro/routes/api/media/providers/[providerId]/[itemId].ts +86 -0
- package/src/astro/routes/api/media/providers/[providerId]/index.ts +111 -0
- package/src/astro/routes/api/media/providers/index.ts +30 -0
- package/src/astro/routes/api/media/upload-url.ts +137 -0
- package/src/astro/routes/api/media.ts +190 -0
- package/src/astro/routes/api/menus/[name]/items.ts +87 -0
- package/src/astro/routes/api/menus/[name]/reorder.ts +33 -0
- package/src/astro/routes/api/menus/[name].ts +65 -0
- package/src/astro/routes/api/menus/index.ts +47 -0
- package/src/astro/routes/api/oauth/authorize.ts +412 -0
- package/src/astro/routes/api/oauth/device/authorize.ts +45 -0
- package/src/astro/routes/api/oauth/device/code.ts +51 -0
- package/src/astro/routes/api/oauth/device/token.ts +69 -0
- package/src/astro/routes/api/oauth/token/refresh.ts +38 -0
- package/src/astro/routes/api/oauth/token/revoke.ts +38 -0
- package/src/astro/routes/api/oauth/token.ts +184 -0
- package/src/astro/routes/api/openapi.json.ts +32 -0
- package/src/astro/routes/api/plugins/[pluginId]/[...path].ts +92 -0
- package/src/astro/routes/api/redirects/404s/index.ts +72 -0
- package/src/astro/routes/api/redirects/404s/summary.ts +33 -0
- package/src/astro/routes/api/redirects/[id].ts +84 -0
- package/src/astro/routes/api/redirects/index.ts +52 -0
- package/src/astro/routes/api/revisions/[revisionId]/index.ts +29 -0
- package/src/astro/routes/api/revisions/[revisionId]/restore.ts +58 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +76 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +52 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +32 -0
- package/src/astro/routes/api/schema/collections/[slug]/index.ts +80 -0
- package/src/astro/routes/api/schema/collections/index.ts +47 -0
- package/src/astro/routes/api/schema/index.ts +109 -0
- package/src/astro/routes/api/schema/orphans/[slug].ts +36 -0
- package/src/astro/routes/api/schema/orphans/index.ts +26 -0
- package/src/astro/routes/api/search/enable.ts +64 -0
- package/src/astro/routes/api/search/index.ts +55 -0
- package/src/astro/routes/api/search/rebuild.ts +72 -0
- package/src/astro/routes/api/search/stats.ts +35 -0
- package/src/astro/routes/api/search/suggest.ts +53 -0
- package/src/astro/routes/api/sections/[slug].ts +84 -0
- package/src/astro/routes/api/sections/index.ts +52 -0
- package/src/astro/routes/api/settings/email.ts +150 -0
- package/src/astro/routes/api/settings.ts +67 -0
- package/src/astro/routes/api/setup/admin-verify.ts +100 -0
- package/src/astro/routes/api/setup/admin.ts +94 -0
- package/src/astro/routes/api/setup/dev-bypass.ts +199 -0
- package/src/astro/routes/api/setup/dev-reset.ts +40 -0
- package/src/astro/routes/api/setup/index.ts +126 -0
- package/src/astro/routes/api/setup/status.ts +122 -0
- package/src/astro/routes/api/snapshot.ts +75 -0
- package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +95 -0
- package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +69 -0
- package/src/astro/routes/api/taxonomies/index.ts +59 -0
- package/src/astro/routes/api/themes/preview.ts +77 -0
- package/src/astro/routes/api/typegen.ts +114 -0
- package/src/astro/routes/api/well-known/auth.ts +68 -0
- package/src/astro/routes/api/well-known/oauth-authorization-server.ts +44 -0
- package/src/astro/routes/api/well-known/oauth-protected-resource.ts +37 -0
- package/src/astro/routes/api/widget-areas/[name]/reorder.ts +68 -0
- package/src/astro/routes/api/widget-areas/[name]/widgets/[id].ts +127 -0
- package/src/astro/routes/api/widget-areas/[name]/widgets.ts +80 -0
- package/src/astro/routes/api/widget-areas/[name].ts +87 -0
- package/src/astro/routes/api/widget-areas/index.ts +99 -0
- package/src/astro/routes/api/widget-components.ts +22 -0
- package/src/astro/routes/robots.txt.ts +77 -0
- package/src/astro/routes/sitemap.xml.ts +97 -0
- package/src/astro/storage/adapters.ts +74 -0
- package/src/astro/storage/index.ts +19 -0
- package/src/astro/storage/types.ts +60 -0
- package/src/astro/types.ts +346 -0
- package/src/auth/api-tokens.ts +25 -0
- package/src/auth/challenge-store.ts +80 -0
- package/src/auth/mode.ts +96 -0
- package/src/auth/oauth-state-store.ts +96 -0
- package/src/auth/passkey-config.ts +27 -0
- package/src/auth/rate-limit.ts +158 -0
- package/src/auth/scopes.ts +33 -0
- package/src/auth/types.ts +104 -0
- package/src/aws-sdk.d.ts +100 -0
- package/src/bylines/index.ts +237 -0
- package/src/cleanup.ts +153 -0
- package/src/cli/client-factory.ts +100 -0
- package/src/cli/commands/auth.ts +46 -0
- package/src/cli/commands/bundle-utils.ts +247 -0
- package/src/cli/commands/bundle.ts +609 -0
- package/src/cli/commands/content.ts +442 -0
- package/src/cli/commands/dev.ts +191 -0
- package/src/cli/commands/doctor.ts +211 -0
- package/src/cli/commands/export-seed.ts +630 -0
- package/src/cli/commands/import/wordpress.ts +1056 -0
- package/src/cli/commands/init.ts +192 -0
- package/src/cli/commands/login.ts +547 -0
- package/src/cli/commands/media.ts +165 -0
- package/src/cli/commands/menu.ts +67 -0
- package/src/cli/commands/plugin-init.ts +291 -0
- package/src/cli/commands/plugin-validate.ts +31 -0
- package/src/cli/commands/plugin.ts +33 -0
- package/src/cli/commands/publish.ts +697 -0
- package/src/cli/commands/schema.ts +233 -0
- package/src/cli/commands/search-cmd.ts +54 -0
- package/src/cli/commands/seed.ts +286 -0
- package/src/cli/commands/taxonomy.ts +128 -0
- package/src/cli/commands/types.ts +68 -0
- package/src/cli/credentials.ts +236 -0
- package/src/cli/index.ts +70 -0
- package/src/cli/output.ts +75 -0
- package/src/cli/wxr/parser.ts +969 -0
- package/src/client/cf-access.ts +193 -0
- package/src/client/index.ts +854 -0
- package/src/client/portable-text.ts +413 -0
- package/src/client/transport.ts +200 -0
- package/src/comments/moderator.ts +46 -0
- package/src/comments/notifications.ts +144 -0
- package/src/comments/query.ts +105 -0
- package/src/comments/service.ts +213 -0
- package/src/components/Break.astro +45 -0
- package/src/components/Button.astro +71 -0
- package/src/components/Buttons.astro +49 -0
- package/src/components/Code.astro +59 -0
- package/src/components/Columns.astro +59 -0
- package/src/components/CommentForm.astro +315 -0
- package/src/components/Comments.astro +232 -0
- package/src/components/Cover.astro +128 -0
- package/src/components/EmDashBodyEnd.astro +32 -0
- package/src/components/EmDashBodyStart.astro +32 -0
- package/src/components/EmDashHead.astro +53 -0
- package/src/components/EmDashImage.astro +178 -0
- package/src/components/EmDashMedia.astro +167 -0
- package/src/components/Embed.astro +128 -0
- package/src/components/File.astro +122 -0
- package/src/components/Gallery.astro +93 -0
- package/src/components/HtmlBlock.astro +33 -0
- package/src/components/Image.astro +178 -0
- package/src/components/InlineEditor.astro +27 -0
- package/src/components/InlinePortableTextEditor.tsx +1905 -0
- package/src/components/LiveSearch.astro +614 -0
- package/src/components/PortableText.astro +51 -0
- package/src/components/Pullquote.astro +51 -0
- package/src/components/Table.astro +108 -0
- package/src/components/WidgetArea.astro +22 -0
- package/src/components/WidgetRenderer.astro +72 -0
- package/src/components/index.ts +116 -0
- package/src/components/marks/Link.astro +31 -0
- package/src/components/marks/StrikeThrough.astro +7 -0
- package/src/components/marks/Subscript.astro +7 -0
- package/src/components/marks/Superscript.astro +7 -0
- package/src/components/marks/Underline.astro +7 -0
- package/src/components/widgets/Archives.astro +65 -0
- package/src/components/widgets/Categories.astro +35 -0
- package/src/components/widgets/RecentPosts.astro +51 -0
- package/src/components/widgets/Search.astro +18 -0
- package/src/components/widgets/Tags.astro +38 -0
- package/src/content/converters/index.ts +9 -0
- package/src/content/converters/portable-text-to-prosemirror.ts +385 -0
- package/src/content/converters/prosemirror-to-portable-text.ts +413 -0
- package/src/content/converters/types.ts +120 -0
- package/src/content/index.ts +5 -0
- package/src/database/connection.ts +67 -0
- package/src/database/dialect-helpers.ts +138 -0
- package/src/database/index.ts +5 -0
- package/src/database/migrations/001_initial.ts +170 -0
- package/src/database/migrations/002_media_status.ts +26 -0
- package/src/database/migrations/003_schema_registry.ts +79 -0
- package/src/database/migrations/004_plugins.ts +62 -0
- package/src/database/migrations/005_menus.ts +67 -0
- package/src/database/migrations/006_taxonomy_defs.ts +51 -0
- package/src/database/migrations/007_widgets.ts +42 -0
- package/src/database/migrations/008_auth.ts +194 -0
- package/src/database/migrations/009_user_disabled.ts +27 -0
- package/src/database/migrations/011_sections.ts +65 -0
- package/src/database/migrations/012_search.ts +25 -0
- package/src/database/migrations/013_scheduled_publishing.ts +51 -0
- package/src/database/migrations/014_draft_revisions.ts +72 -0
- package/src/database/migrations/015_indexes.ts +82 -0
- package/src/database/migrations/016_api_tokens.ts +89 -0
- package/src/database/migrations/017_authorization_codes.ts +45 -0
- package/src/database/migrations/018_seo.ts +56 -0
- package/src/database/migrations/019_i18n.ts +618 -0
- package/src/database/migrations/020_collection_url_pattern.ts +23 -0
- package/src/database/migrations/021_remove_section_categories.ts +43 -0
- package/src/database/migrations/022_marketplace_plugin_state.ts +46 -0
- package/src/database/migrations/023_plugin_metadata.ts +33 -0
- package/src/database/migrations/024_media_placeholders.ts +32 -0
- package/src/database/migrations/025_oauth_clients.ts +28 -0
- package/src/database/migrations/026_cron_tasks.ts +49 -0
- package/src/database/migrations/027_comments.ts +87 -0
- package/src/database/migrations/028_drop_author_url.ts +9 -0
- package/src/database/migrations/029_redirects.ts +67 -0
- package/src/database/migrations/030_widen_scheduled_index.ts +48 -0
- package/src/database/migrations/031_bylines.ts +90 -0
- package/src/database/migrations/032_rate_limits.ts +39 -0
- package/src/database/migrations/runner.ts +170 -0
- package/src/database/repositories/audit.ts +294 -0
- package/src/database/repositories/byline.ts +387 -0
- package/src/database/repositories/comment.ts +458 -0
- package/src/database/repositories/content.ts +1144 -0
- package/src/database/repositories/index.ts +30 -0
- package/src/database/repositories/media.ts +347 -0
- package/src/database/repositories/options.ts +150 -0
- package/src/database/repositories/plugin-storage.ts +373 -0
- package/src/database/repositories/redirect.ts +480 -0
- package/src/database/repositories/revision.ts +200 -0
- package/src/database/repositories/seo.ts +176 -0
- package/src/database/repositories/taxonomy.ts +294 -0
- package/src/database/repositories/types.ts +132 -0
- package/src/database/repositories/user.ts +258 -0
- package/src/database/transaction.ts +54 -0
- package/src/database/types.ts +501 -0
- package/src/database/validate.ts +138 -0
- package/src/db/adapters.ts +125 -0
- package/src/db/index.ts +37 -0
- package/src/db/libsql.ts +23 -0
- package/src/db/postgres.ts +30 -0
- package/src/db/sqlite.ts +27 -0
- package/src/emdash-runtime.ts +2096 -0
- package/src/fields/boolean.ts +34 -0
- package/src/fields/datetime.ts +44 -0
- package/src/fields/file.ts +41 -0
- package/src/fields/image.ts +34 -0
- package/src/fields/index.ts +42 -0
- package/src/fields/integer.ts +50 -0
- package/src/fields/json.ts +37 -0
- package/src/fields/multiselect.ts +48 -0
- package/src/fields/number.ts +52 -0
- package/src/fields/portable-text.ts +33 -0
- package/src/fields/reference.ts +29 -0
- package/src/fields/richtext.ts +31 -0
- package/src/fields/select.ts +46 -0
- package/src/fields/slug.ts +38 -0
- package/src/fields/text.ts +55 -0
- package/src/fields/textarea.ts +52 -0
- package/src/fields/types.ts +64 -0
- package/src/i18n/config.ts +68 -0
- package/src/import/index.ts +90 -0
- package/src/import/menus.ts +436 -0
- package/src/import/registry.ts +111 -0
- package/src/import/sections.ts +103 -0
- package/src/import/settings.ts +281 -0
- package/src/import/sources/wordpress-plugin.ts +641 -0
- package/src/import/sources/wordpress-rest.ts +191 -0
- package/src/import/sources/wxr.ts +330 -0
- package/src/import/ssrf.ts +260 -0
- package/src/import/types.ts +418 -0
- package/src/import/utils.ts +412 -0
- package/src/index.ts +481 -0
- package/src/loader.ts +770 -0
- package/src/mcp/server.ts +1463 -0
- package/src/media/index.ts +32 -0
- package/src/media/local-runtime.ts +213 -0
- package/src/media/local.ts +46 -0
- package/src/media/normalize.ts +190 -0
- package/src/media/placeholder.ts +150 -0
- package/src/media/provider-loader.ts +78 -0
- package/src/media/types.ts +279 -0
- package/src/menus/index.ts +324 -0
- package/src/menus/types.ts +112 -0
- package/src/page/context.ts +93 -0
- package/src/page/fragments.ts +89 -0
- package/src/page/index.ts +58 -0
- package/src/page/jsonld.ts +94 -0
- package/src/page/metadata.ts +185 -0
- package/src/page/seo-contributions.ts +136 -0
- package/src/plugin-utils.ts +80 -0
- package/src/plugins/adapt-sandbox-entry.ts +207 -0
- package/src/plugins/context.ts +833 -0
- package/src/plugins/cron.ts +361 -0
- package/src/plugins/define-plugin.ts +259 -0
- package/src/plugins/email-console.ts +73 -0
- package/src/plugins/email.ts +209 -0
- package/src/plugins/hooks.ts +1273 -0
- package/src/plugins/index.ts +193 -0
- package/src/plugins/manager.ts +595 -0
- package/src/plugins/manifest-schema.ts +230 -0
- package/src/plugins/marketplace.ts +460 -0
- package/src/plugins/request-meta.ts +139 -0
- package/src/plugins/routes.ts +302 -0
- package/src/plugins/sandbox/index.ts +18 -0
- package/src/plugins/sandbox/noop.ts +76 -0
- package/src/plugins/sandbox/types.ts +173 -0
- package/src/plugins/scheduler/node.ts +122 -0
- package/src/plugins/scheduler/piggyback.ts +71 -0
- package/src/plugins/scheduler/types.ts +27 -0
- package/src/plugins/state.ts +208 -0
- package/src/plugins/storage-indexes.ts +326 -0
- package/src/plugins/storage-query.ts +240 -0
- package/src/plugins/types.ts +1284 -0
- package/src/preview/helpers.ts +27 -0
- package/src/preview/index.ts +40 -0
- package/src/preview/tokens.ts +279 -0
- package/src/preview/urls.ts +118 -0
- package/src/query.ts +674 -0
- package/src/redirects/patterns.ts +224 -0
- package/src/request-context.ts +67 -0
- package/src/runtime.ts +21 -0
- package/src/schema/index.ts +29 -0
- package/src/schema/query.ts +44 -0
- package/src/schema/registry.ts +965 -0
- package/src/schema/types.ts +276 -0
- package/src/schema/zod-generator.ts +413 -0
- package/src/search/fts-manager.ts +452 -0
- package/src/search/index.ts +26 -0
- package/src/search/query.ts +396 -0
- package/src/search/text-extraction.ts +162 -0
- package/src/search/types.ts +114 -0
- package/src/sections/index.ts +226 -0
- package/src/sections/types.ts +86 -0
- package/src/seed/apply.ts +1141 -0
- package/src/seed/default.ts +86 -0
- package/src/seed/index.ts +28 -0
- package/src/seed/load.ts +35 -0
- package/src/seed/types.ts +341 -0
- package/src/seed/validate.ts +642 -0
- package/src/seo/index.ts +179 -0
- package/src/settings/index.ts +203 -0
- package/src/settings/types.ts +58 -0
- package/src/storage/index.ts +28 -0
- package/src/storage/local.ts +249 -0
- package/src/storage/s3.ts +263 -0
- package/src/storage/types.ts +204 -0
- package/src/taxonomies/index.ts +309 -0
- package/src/taxonomies/types.ts +61 -0
- package/src/ui.ts +75 -0
- package/src/utils/base64.ts +73 -0
- package/src/utils/hash.ts +36 -0
- package/src/utils/sanitize.ts +20 -0
- package/src/utils/slugify.ts +29 -0
- package/src/utils/url.ts +48 -0
- package/src/virtual-modules.d.ts +111 -0
- package/src/visual-editing/editable.ts +108 -0
- package/src/visual-editing/toolbar.ts +1229 -0
- package/src/widgets/components.ts +105 -0
- package/src/widgets/index.ts +131 -0
- package/src/widgets/types.ts +81 -0
|
@@ -0,0 +1,743 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth middleware for admin routes
|
|
3
|
+
*
|
|
4
|
+
* Checks if the user is authenticated and has appropriate permissions.
|
|
5
|
+
* Supports two auth modes:
|
|
6
|
+
* - Passkey (default): Session-based auth with passkey login
|
|
7
|
+
* - External providers: JWT-based auth (Cloudflare Access, etc.)
|
|
8
|
+
*
|
|
9
|
+
* This middleware runs AFTER the setup middleware - so if we get here,
|
|
10
|
+
* we know setup is complete and users exist.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { User, RoleLevel } from "@emdash-cms/auth";
|
|
14
|
+
import { createKyselyAdapter } from "@emdash-cms/auth/adapters/kysely";
|
|
15
|
+
import { defineMiddleware } from "astro:middleware";
|
|
16
|
+
import { ulid } from "ulidx";
|
|
17
|
+
// Import auth provider via virtual module (statically bundled)
|
|
18
|
+
// This avoids dynamic import issues in Cloudflare Workers
|
|
19
|
+
import { authenticate as virtualAuthenticate } from "virtual:emdash/auth";
|
|
20
|
+
|
|
21
|
+
import { checkPublicCsrf } from "../../api/csrf.js";
|
|
22
|
+
import { apiError } from "../../api/error.js";
|
|
23
|
+
|
|
24
|
+
/** Cache headers for middleware error responses (matches API_CACHE_HEADERS in api/error.ts) */
|
|
25
|
+
const MW_CACHE_HEADERS = {
|
|
26
|
+
"Cache-Control": "private, no-store",
|
|
27
|
+
} as const;
|
|
28
|
+
import { resolveApiToken, resolveOAuthToken } from "../../api/handlers/api-tokens.js";
|
|
29
|
+
import { hasScope } from "../../auth/api-tokens.js";
|
|
30
|
+
import { getAuthMode, type ExternalAuthMode } from "../../auth/mode.js";
|
|
31
|
+
import type { ExternalAuthConfig } from "../../auth/types.js";
|
|
32
|
+
import type { EmDashHandlers, EmDashManifest } from "../types.js";
|
|
33
|
+
|
|
34
|
+
declare global {
|
|
35
|
+
namespace App {
|
|
36
|
+
interface Locals {
|
|
37
|
+
user?: User;
|
|
38
|
+
/** Token scopes when authenticated via API token or OAuth token. Undefined for session auth. */
|
|
39
|
+
tokenScopes?: string[];
|
|
40
|
+
emdash?: EmDashHandlers;
|
|
41
|
+
emdashManifest?: EmDashManifest;
|
|
42
|
+
}
|
|
43
|
+
interface SessionData {
|
|
44
|
+
user: { id: string };
|
|
45
|
+
hasSeenWelcome: boolean;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Role level constants (matching @emdash-cms/auth)
|
|
51
|
+
const ROLE_ADMIN = 50;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Strict Content-Security-Policy for /_emdash routes (admin + API).
|
|
55
|
+
*
|
|
56
|
+
* Applied via middleware header rather than Astro's built-in CSP because
|
|
57
|
+
* Astro's auto-hashing defeats 'unsafe-inline' (CSP3 ignores 'unsafe-inline'
|
|
58
|
+
* when hashes are present), which would break user-facing pages.
|
|
59
|
+
*/
|
|
60
|
+
function buildEmDashCsp(marketplaceUrl?: string): string {
|
|
61
|
+
const imgSources = ["'self'", "data:", "blob:"];
|
|
62
|
+
if (marketplaceUrl) {
|
|
63
|
+
try {
|
|
64
|
+
imgSources.push(new URL(marketplaceUrl).origin);
|
|
65
|
+
} catch {
|
|
66
|
+
// ignore invalid marketplace URL
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return [
|
|
70
|
+
"default-src 'self'",
|
|
71
|
+
"script-src 'self' 'unsafe-inline'",
|
|
72
|
+
"style-src 'self' 'unsafe-inline'",
|
|
73
|
+
"connect-src 'self'",
|
|
74
|
+
"form-action 'self'",
|
|
75
|
+
"frame-ancestors 'none'",
|
|
76
|
+
`img-src ${imgSources.join(" ")}`,
|
|
77
|
+
"object-src 'none'",
|
|
78
|
+
"base-uri 'self'",
|
|
79
|
+
].join("; ");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* API routes that skip auth — each handles its own access control.
|
|
84
|
+
*
|
|
85
|
+
* Prefix entries match any path starting with that prefix.
|
|
86
|
+
* Exact entries (no trailing slash or wildcard) match that path only.
|
|
87
|
+
*/
|
|
88
|
+
const PUBLIC_API_PREFIXES = [
|
|
89
|
+
"/_emdash/api/setup",
|
|
90
|
+
"/_emdash/api/auth/login",
|
|
91
|
+
"/_emdash/api/auth/register",
|
|
92
|
+
"/_emdash/api/auth/dev-bypass",
|
|
93
|
+
"/_emdash/api/auth/signup/",
|
|
94
|
+
"/_emdash/api/auth/magic-link/",
|
|
95
|
+
"/_emdash/api/auth/invite/accept",
|
|
96
|
+
"/_emdash/api/auth/invite/complete",
|
|
97
|
+
"/_emdash/api/auth/oauth/",
|
|
98
|
+
"/_emdash/api/oauth/device/token",
|
|
99
|
+
"/_emdash/api/oauth/device/code",
|
|
100
|
+
"/_emdash/api/oauth/token",
|
|
101
|
+
"/_emdash/api/comments/",
|
|
102
|
+
"/_emdash/api/media/file/",
|
|
103
|
+
"/_emdash/.well-known/",
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
const PUBLIC_API_EXACT = new Set([
|
|
107
|
+
"/_emdash/api/auth/passkey/options",
|
|
108
|
+
"/_emdash/api/auth/passkey/verify",
|
|
109
|
+
"/_emdash/api/oauth/token",
|
|
110
|
+
"/_emdash/api/snapshot",
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
function isPublicEmDashRoute(pathname: string): boolean {
|
|
114
|
+
if (PUBLIC_API_EXACT.has(pathname)) return true;
|
|
115
|
+
if (PUBLIC_API_PREFIXES.some((p) => pathname.startsWith(p))) return true;
|
|
116
|
+
if (import.meta.env.DEV && pathname === "/_emdash/api/typegen") return true;
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const onRequest = defineMiddleware(async (context, next) => {
|
|
121
|
+
const { url } = context;
|
|
122
|
+
|
|
123
|
+
// Only check auth on admin routes and API routes
|
|
124
|
+
const isAdminRoute = url.pathname.startsWith("/_emdash/admin");
|
|
125
|
+
const isSetupRoute = url.pathname.startsWith("/_emdash/admin/setup");
|
|
126
|
+
const isApiRoute = url.pathname.startsWith("/_emdash/api");
|
|
127
|
+
const isPublicApiRoute = isPublicEmDashRoute(url.pathname);
|
|
128
|
+
|
|
129
|
+
const isPublicRoute = !isAdminRoute && !isApiRoute;
|
|
130
|
+
|
|
131
|
+
// Public API routes skip auth but still need CSRF protection on state-changing methods.
|
|
132
|
+
// We check Origin header against the request host (same approach as Astro's checkOrigin).
|
|
133
|
+
// This prevents cross-origin form submissions and fetch requests from malicious sites.
|
|
134
|
+
if (isPublicApiRoute) {
|
|
135
|
+
const method = context.request.method.toUpperCase();
|
|
136
|
+
if (method !== "GET" && method !== "HEAD" && method !== "OPTIONS") {
|
|
137
|
+
const csrfError = checkPublicCsrf(context.request, url);
|
|
138
|
+
if (csrfError) return csrfError;
|
|
139
|
+
}
|
|
140
|
+
return next();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Plugin routes: soft auth (resolve user if credentials present, but never block).
|
|
144
|
+
// The catch-all handler decides per-route whether auth is required (public vs private).
|
|
145
|
+
// Public plugin routes that accept POST are vulnerable to cross-origin form submissions,
|
|
146
|
+
// so we apply the same Origin-based CSRF check as other public routes.
|
|
147
|
+
const isPluginRoute = url.pathname.startsWith("/_emdash/api/plugins/");
|
|
148
|
+
if (isPluginRoute) {
|
|
149
|
+
const method = context.request.method.toUpperCase();
|
|
150
|
+
if (method !== "GET" && method !== "HEAD" && method !== "OPTIONS") {
|
|
151
|
+
const csrfError = checkPublicCsrf(context.request, url);
|
|
152
|
+
if (csrfError) return csrfError;
|
|
153
|
+
}
|
|
154
|
+
return handlePluginRouteAuth(context, next);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Setup routes: skip auth but still enforce CSRF on state-changing methods
|
|
158
|
+
if (isSetupRoute) {
|
|
159
|
+
const method = context.request.method.toUpperCase();
|
|
160
|
+
if (method !== "GET" && method !== "HEAD" && method !== "OPTIONS") {
|
|
161
|
+
const csrfHeader = context.request.headers.get("X-EmDash-Request");
|
|
162
|
+
if (csrfHeader !== "1") {
|
|
163
|
+
return new Response(
|
|
164
|
+
JSON.stringify({
|
|
165
|
+
error: { code: "CSRF_REJECTED", message: "Missing required header" },
|
|
166
|
+
}),
|
|
167
|
+
{
|
|
168
|
+
status: 403,
|
|
169
|
+
headers: { "Content-Type": "application/json", ...MW_CACHE_HEADERS },
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return next();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// For public routes: soft auth check (set locals.user if session exists, but never block)
|
|
178
|
+
if (isPublicRoute) {
|
|
179
|
+
return handlePublicRouteAuth(context, next);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// --- Everything below is /_emdash (admin + API) ---
|
|
183
|
+
|
|
184
|
+
// Try Bearer token auth first (API tokens and OAuth tokens).
|
|
185
|
+
// If successful, skip CSRF (tokens aren't ambient credentials like cookies).
|
|
186
|
+
const bearerResult = await handleBearerAuth(context);
|
|
187
|
+
|
|
188
|
+
if (bearerResult === "invalid") {
|
|
189
|
+
const headers: Record<string, string> = {
|
|
190
|
+
"Content-Type": "application/json",
|
|
191
|
+
...MW_CACHE_HEADERS,
|
|
192
|
+
};
|
|
193
|
+
// Add WWW-Authenticate header on MCP endpoint 401s to trigger OAuth discovery
|
|
194
|
+
if (url.pathname === "/_emdash/api/mcp") {
|
|
195
|
+
headers["WWW-Authenticate"] =
|
|
196
|
+
`Bearer resource_metadata="${url.origin}/.well-known/oauth-protected-resource"`;
|
|
197
|
+
}
|
|
198
|
+
return new Response(
|
|
199
|
+
JSON.stringify({ error: { code: "INVALID_TOKEN", message: "Invalid or expired token" } }),
|
|
200
|
+
{ status: 401, headers },
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const isTokenAuth = bearerResult === "authenticated";
|
|
205
|
+
|
|
206
|
+
// CSRF protection: require X-EmDash-Request header on state-changing requests.
|
|
207
|
+
// Skip for token-authenticated requests (tokens aren't ambient credentials).
|
|
208
|
+
// Browsers block cross-origin custom headers, so this prevents CSRF without tokens.
|
|
209
|
+
// OAuth authorize consent is exempt: it's a standard HTML form POST that can't
|
|
210
|
+
// include custom headers. The consent flow is protected by session + single-use codes.
|
|
211
|
+
const method = context.request.method.toUpperCase();
|
|
212
|
+
const isOAuthConsent = url.pathname.startsWith("/_emdash/oauth/authorize");
|
|
213
|
+
if (
|
|
214
|
+
isApiRoute &&
|
|
215
|
+
!isTokenAuth &&
|
|
216
|
+
!isOAuthConsent &&
|
|
217
|
+
method !== "GET" &&
|
|
218
|
+
method !== "HEAD" &&
|
|
219
|
+
method !== "OPTIONS" &&
|
|
220
|
+
!isPublicApiRoute
|
|
221
|
+
) {
|
|
222
|
+
const csrfHeader = context.request.headers.get("X-EmDash-Request");
|
|
223
|
+
if (csrfHeader !== "1") {
|
|
224
|
+
return new Response(
|
|
225
|
+
JSON.stringify({ error: { code: "CSRF_REJECTED", message: "Missing required header" } }),
|
|
226
|
+
{
|
|
227
|
+
status: 403,
|
|
228
|
+
headers: { "Content-Type": "application/json", ...MW_CACHE_HEADERS },
|
|
229
|
+
},
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// If already authenticated via Bearer token, enforce scope then skip session/external auth
|
|
235
|
+
if (isTokenAuth) {
|
|
236
|
+
// Enforce API token scopes based on URL pattern + HTTP method
|
|
237
|
+
const scopeError = enforceTokenScope(url.pathname, method, context.locals.tokenScopes);
|
|
238
|
+
if (scopeError) return scopeError;
|
|
239
|
+
|
|
240
|
+
const response = await next();
|
|
241
|
+
if (!import.meta.env.DEV) {
|
|
242
|
+
const marketplaceUrl = context.locals.emdash?.config.marketplace;
|
|
243
|
+
response.headers.set("Content-Security-Policy", buildEmDashCsp(marketplaceUrl));
|
|
244
|
+
}
|
|
245
|
+
return response;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const response = await handleEmDashAuth(context, next);
|
|
249
|
+
|
|
250
|
+
// Set strict CSP on all /_emdash responses (prod only)
|
|
251
|
+
if (!import.meta.env.DEV) {
|
|
252
|
+
const marketplaceUrl = context.locals.emdash?.config.marketplace;
|
|
253
|
+
response.headers.set("Content-Security-Policy", buildEmDashCsp(marketplaceUrl));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return response;
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Auth handling for /_emdash routes. Returns a Response from either
|
|
261
|
+
* an auth error/redirect or the downstream route handler.
|
|
262
|
+
*/
|
|
263
|
+
async function handleEmDashAuth(
|
|
264
|
+
context: Parameters<Parameters<typeof defineMiddleware>[0]>[0],
|
|
265
|
+
next: Parameters<Parameters<typeof defineMiddleware>[0]>[1],
|
|
266
|
+
): Promise<Response> {
|
|
267
|
+
const { url, locals } = context;
|
|
268
|
+
const { emdash } = locals;
|
|
269
|
+
|
|
270
|
+
const isLoginRoute = url.pathname.startsWith("/_emdash/admin/login");
|
|
271
|
+
const isApiRoute = url.pathname.startsWith("/_emdash/api");
|
|
272
|
+
|
|
273
|
+
if (!emdash?.db) {
|
|
274
|
+
// No database - let the admin handle this error
|
|
275
|
+
return next();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Determine auth mode from config
|
|
279
|
+
const authMode = getAuthMode(emdash.config);
|
|
280
|
+
|
|
281
|
+
if (authMode.type === "external") {
|
|
282
|
+
// In dev mode, fall back to passkey auth since external JWT won't be present
|
|
283
|
+
if (import.meta.env.DEV) {
|
|
284
|
+
if (isLoginRoute) {
|
|
285
|
+
return next();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return handlePasskeyAuth(context, next, isApiRoute);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// External auth provider (Cloudflare Access, etc.)
|
|
292
|
+
return handleExternalAuth(context, next, authMode, isApiRoute);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Passkey authentication (default)
|
|
296
|
+
if (isLoginRoute) {
|
|
297
|
+
return next();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return handlePasskeyAuth(context, next, isApiRoute);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Soft auth for plugin routes: resolve user from Bearer token or session if present,
|
|
305
|
+
* but never block unauthenticated requests. The catch-all handler checks route
|
|
306
|
+
* metadata to decide whether auth is required (public vs private routes).
|
|
307
|
+
*/
|
|
308
|
+
async function handlePluginRouteAuth(
|
|
309
|
+
context: Parameters<Parameters<typeof defineMiddleware>[0]>[0],
|
|
310
|
+
next: Parameters<Parameters<typeof defineMiddleware>[0]>[1],
|
|
311
|
+
): Promise<Response> {
|
|
312
|
+
const { locals } = context;
|
|
313
|
+
const { emdash } = locals;
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
// Try Bearer token auth first (API tokens and OAuth tokens)
|
|
317
|
+
const bearerResult = await handleBearerAuth(context);
|
|
318
|
+
if (bearerResult === "authenticated") {
|
|
319
|
+
// User and tokenScopes are set on locals by handleBearerAuth
|
|
320
|
+
return next();
|
|
321
|
+
}
|
|
322
|
+
if (bearerResult === "invalid") {
|
|
323
|
+
// A token was presented but is invalid/expired — return 401 so the
|
|
324
|
+
// caller knows their token is bad (don't silently downgrade to no-auth).
|
|
325
|
+
return new Response(
|
|
326
|
+
JSON.stringify({ error: { code: "INVALID_TOKEN", message: "Invalid or expired token" } }),
|
|
327
|
+
{
|
|
328
|
+
status: 401,
|
|
329
|
+
headers: { "Content-Type": "application/json", ...MW_CACHE_HEADERS },
|
|
330
|
+
},
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
// "none" — no token presented, try session auth below.
|
|
334
|
+
} catch (error) {
|
|
335
|
+
console.error("Plugin route bearer auth error:", error);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
try {
|
|
339
|
+
// Try session auth (sets locals.user if session exists)
|
|
340
|
+
const { session } = context;
|
|
341
|
+
const sessionUser = await session?.get("user");
|
|
342
|
+
if (sessionUser?.id && emdash?.db) {
|
|
343
|
+
const adapter = createKyselyAdapter(emdash.db);
|
|
344
|
+
const user = await adapter.getUserById(sessionUser.id);
|
|
345
|
+
if (user && !user.disabled) {
|
|
346
|
+
locals.user = user;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
} catch (error) {
|
|
350
|
+
// Log but don't block — public routes should still work without session
|
|
351
|
+
console.error("Plugin route session auth error:", error);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return next();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Soft auth check for public routes with edit mode cookie.
|
|
359
|
+
* Checks the session and sets locals.user if valid, but never blocks the request.
|
|
360
|
+
*/
|
|
361
|
+
async function handlePublicRouteAuth(
|
|
362
|
+
context: Parameters<Parameters<typeof defineMiddleware>[0]>[0],
|
|
363
|
+
next: Parameters<Parameters<typeof defineMiddleware>[0]>[1],
|
|
364
|
+
): Promise<Response> {
|
|
365
|
+
const { locals, session } = context;
|
|
366
|
+
const { emdash } = locals;
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
const sessionUser = await session?.get("user");
|
|
370
|
+
if (sessionUser?.id && emdash?.db) {
|
|
371
|
+
const adapter = createKyselyAdapter(emdash.db);
|
|
372
|
+
const user = await adapter.getUserById(sessionUser.id);
|
|
373
|
+
if (user && !user.disabled) {
|
|
374
|
+
locals.user = user;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
} catch {
|
|
378
|
+
// Silently continue — public page should render normally
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return next();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Handle external auth provider authentication (Cloudflare Access, etc.)
|
|
386
|
+
*/
|
|
387
|
+
async function handleExternalAuth(
|
|
388
|
+
context: Parameters<Parameters<typeof defineMiddleware>[0]>[0],
|
|
389
|
+
next: Parameters<Parameters<typeof defineMiddleware>[0]>[1],
|
|
390
|
+
authMode: ExternalAuthMode,
|
|
391
|
+
_isApiRoute: boolean,
|
|
392
|
+
): Promise<Response> {
|
|
393
|
+
const { locals, request } = context;
|
|
394
|
+
const { emdash } = locals;
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
// Use the authenticate function from the virtual module
|
|
398
|
+
// (statically imported at build time to work with Cloudflare Workers)
|
|
399
|
+
if (typeof virtualAuthenticate !== "function") {
|
|
400
|
+
throw new Error(
|
|
401
|
+
`Auth provider ${authMode.entrypoint} does not export an authenticate function`,
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Authenticate via the provider
|
|
406
|
+
const authResult = await virtualAuthenticate(request, authMode.config);
|
|
407
|
+
|
|
408
|
+
// Get external auth config for auto-provision settings
|
|
409
|
+
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- narrowing AuthModeConfig to ExternalAuthConfig after provider check
|
|
410
|
+
const externalConfig = authMode.config as ExternalAuthConfig;
|
|
411
|
+
|
|
412
|
+
// Find or create user
|
|
413
|
+
const adapter = createKyselyAdapter(emdash!.db);
|
|
414
|
+
let user = await adapter.getUserByEmail(authResult.email);
|
|
415
|
+
|
|
416
|
+
if (!user) {
|
|
417
|
+
// User doesn't exist
|
|
418
|
+
if (externalConfig.autoProvision === false) {
|
|
419
|
+
return new Response("User not authorized", {
|
|
420
|
+
status: 403,
|
|
421
|
+
headers: { "Content-Type": "text/plain", ...MW_CACHE_HEADERS },
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Check if this is the first user (they become admin)
|
|
426
|
+
const userCount = await emdash!.db
|
|
427
|
+
.selectFrom("users")
|
|
428
|
+
.select(emdash!.db.fn.count("id").as("count"))
|
|
429
|
+
.executeTakeFirst();
|
|
430
|
+
|
|
431
|
+
const isFirstUser = Number(userCount?.count ?? 0) === 0;
|
|
432
|
+
const role = isFirstUser ? ROLE_ADMIN : authResult.role;
|
|
433
|
+
|
|
434
|
+
// Create user
|
|
435
|
+
const now = new Date().toISOString();
|
|
436
|
+
const newUser = {
|
|
437
|
+
id: ulid(),
|
|
438
|
+
email: authResult.email,
|
|
439
|
+
name: authResult.name,
|
|
440
|
+
role,
|
|
441
|
+
email_verified: 1,
|
|
442
|
+
created_at: now,
|
|
443
|
+
updated_at: now,
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
await emdash!.db.insertInto("users").values(newUser).execute();
|
|
447
|
+
|
|
448
|
+
user = await adapter.getUserByEmail(authResult.email);
|
|
449
|
+
|
|
450
|
+
console.log(
|
|
451
|
+
`[external-auth] Provisioned user: ${authResult.email} (role: ${role}, first: ${isFirstUser})`,
|
|
452
|
+
);
|
|
453
|
+
} else {
|
|
454
|
+
// User exists - check if we need to sync anything
|
|
455
|
+
const updates: Record<string, unknown> = {};
|
|
456
|
+
let newName: string | undefined;
|
|
457
|
+
let newRole: RoleLevel | undefined;
|
|
458
|
+
|
|
459
|
+
// Sync name from provider if provider provides one and local differs
|
|
460
|
+
if (authResult.name && user.name !== authResult.name) {
|
|
461
|
+
newName = authResult.name;
|
|
462
|
+
updates.name = newName;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Sync role if enabled
|
|
466
|
+
if (externalConfig.syncRoles && user.role !== authResult.role) {
|
|
467
|
+
newRole = authResult.role;
|
|
468
|
+
updates.role = newRole;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (Object.keys(updates).length > 0) {
|
|
472
|
+
updates.updated_at = new Date().toISOString();
|
|
473
|
+
await emdash!.db.updateTable("users").set(updates).where("id", "=", user.id).execute();
|
|
474
|
+
|
|
475
|
+
user = {
|
|
476
|
+
...user,
|
|
477
|
+
...(newName ? { name: newName } : {}),
|
|
478
|
+
...(newRole ? { role: newRole } : {}),
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
console.log(
|
|
482
|
+
`[external-auth] Updated user ${authResult.email}:`,
|
|
483
|
+
Object.keys(updates).filter((k) => k !== "updated_at"),
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (!user) {
|
|
489
|
+
// This shouldn't happen, but handle it gracefully
|
|
490
|
+
return new Response("Failed to provision user", {
|
|
491
|
+
status: 500,
|
|
492
|
+
headers: { "Content-Type": "text/plain", ...MW_CACHE_HEADERS },
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Check if user is disabled locally
|
|
497
|
+
if (user.disabled) {
|
|
498
|
+
return new Response("Account disabled", {
|
|
499
|
+
status: 403,
|
|
500
|
+
headers: { "Content-Type": "text/plain", ...MW_CACHE_HEADERS },
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Set user in locals
|
|
505
|
+
locals.user = user;
|
|
506
|
+
|
|
507
|
+
// Persist to session so public pages can identify the user
|
|
508
|
+
// (external auth headers are only verified on /_emdash routes)
|
|
509
|
+
const { session } = context;
|
|
510
|
+
session?.set("user", { id: user.id });
|
|
511
|
+
|
|
512
|
+
return next();
|
|
513
|
+
} catch (error) {
|
|
514
|
+
console.error("[external-auth] Auth error:", error);
|
|
515
|
+
|
|
516
|
+
return new Response("Authentication failed", {
|
|
517
|
+
status: 401,
|
|
518
|
+
headers: { "Content-Type": "text/plain", ...MW_CACHE_HEADERS },
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Try to authenticate via Bearer token (API token or OAuth token).
|
|
525
|
+
*
|
|
526
|
+
* Returns:
|
|
527
|
+
* - "authenticated" if token is valid and user is resolved
|
|
528
|
+
* - "invalid" if a token was provided but is invalid/expired
|
|
529
|
+
* - "none" if no Bearer token was provided
|
|
530
|
+
*/
|
|
531
|
+
async function handleBearerAuth(
|
|
532
|
+
context: Parameters<Parameters<typeof defineMiddleware>[0]>[0],
|
|
533
|
+
): Promise<"authenticated" | "invalid" | "none"> {
|
|
534
|
+
const authHeader = context.request.headers.get("Authorization");
|
|
535
|
+
if (!authHeader?.startsWith("Bearer ")) return "none";
|
|
536
|
+
|
|
537
|
+
const token = authHeader.slice(7);
|
|
538
|
+
if (!token) return "none";
|
|
539
|
+
|
|
540
|
+
const { locals } = context;
|
|
541
|
+
const { emdash } = locals;
|
|
542
|
+
if (!emdash?.db) return "none";
|
|
543
|
+
|
|
544
|
+
// Resolve token based on prefix
|
|
545
|
+
let resolved: { userId: string; scopes: string[] } | null = null;
|
|
546
|
+
|
|
547
|
+
if (token.startsWith("ec_pat_")) {
|
|
548
|
+
resolved = await resolveApiToken(emdash.db, token);
|
|
549
|
+
} else if (token.startsWith("ec_oat_")) {
|
|
550
|
+
resolved = await resolveOAuthToken(emdash.db, token);
|
|
551
|
+
} else {
|
|
552
|
+
// Unknown token format
|
|
553
|
+
return "invalid";
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (!resolved) return "invalid";
|
|
557
|
+
|
|
558
|
+
// Look up the user
|
|
559
|
+
const adapter = createKyselyAdapter(emdash.db);
|
|
560
|
+
const user = await adapter.getUserById(resolved.userId);
|
|
561
|
+
|
|
562
|
+
if (!user || user.disabled) return "invalid";
|
|
563
|
+
|
|
564
|
+
// Set user and scopes on locals
|
|
565
|
+
locals.user = user;
|
|
566
|
+
locals.tokenScopes = resolved.scopes;
|
|
567
|
+
|
|
568
|
+
return "authenticated";
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Handle passkey (session-based) authentication
|
|
573
|
+
*/
|
|
574
|
+
async function handlePasskeyAuth(
|
|
575
|
+
context: Parameters<Parameters<typeof defineMiddleware>[0]>[0],
|
|
576
|
+
next: Parameters<Parameters<typeof defineMiddleware>[0]>[1],
|
|
577
|
+
isApiRoute: boolean,
|
|
578
|
+
): Promise<Response> {
|
|
579
|
+
const { url, locals, session } = context;
|
|
580
|
+
const { emdash } = locals;
|
|
581
|
+
|
|
582
|
+
try {
|
|
583
|
+
// Check session for user (session.get returns a Promise)
|
|
584
|
+
const sessionUser = await session?.get("user");
|
|
585
|
+
|
|
586
|
+
if (!sessionUser?.id) {
|
|
587
|
+
// Not authenticated
|
|
588
|
+
if (isApiRoute) {
|
|
589
|
+
const headers: Record<string, string> = { ...MW_CACHE_HEADERS };
|
|
590
|
+
// Add WWW-Authenticate on MCP endpoint 401s to trigger OAuth discovery
|
|
591
|
+
if (url.pathname === "/_emdash/api/mcp") {
|
|
592
|
+
headers["WWW-Authenticate"] =
|
|
593
|
+
`Bearer resource_metadata="${url.origin}/.well-known/oauth-protected-resource"`;
|
|
594
|
+
}
|
|
595
|
+
return Response.json(
|
|
596
|
+
{ error: { code: "NOT_AUTHENTICATED", message: "Not authenticated" } },
|
|
597
|
+
{ status: 401, headers },
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
const loginUrl = new URL("/_emdash/admin/login", url.origin);
|
|
601
|
+
loginUrl.searchParams.set("redirect", url.pathname);
|
|
602
|
+
return context.redirect(loginUrl.toString());
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Get full user from database
|
|
606
|
+
const adapter = createKyselyAdapter(emdash!.db);
|
|
607
|
+
const user = await adapter.getUserById(sessionUser.id);
|
|
608
|
+
|
|
609
|
+
if (!user) {
|
|
610
|
+
// User no longer exists - clear session
|
|
611
|
+
session?.destroy();
|
|
612
|
+
if (isApiRoute) {
|
|
613
|
+
return Response.json(
|
|
614
|
+
{ error: { code: "NOT_FOUND", message: "User not found" } },
|
|
615
|
+
{ status: 401, headers: MW_CACHE_HEADERS },
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
return context.redirect("/_emdash/admin/login");
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Check if user is disabled
|
|
622
|
+
if (user.disabled) {
|
|
623
|
+
session?.destroy();
|
|
624
|
+
if (isApiRoute) {
|
|
625
|
+
return apiError("ACCOUNT_DISABLED", "Account disabled", 403);
|
|
626
|
+
}
|
|
627
|
+
const loginUrl = new URL("/_emdash/admin/login", url.origin);
|
|
628
|
+
loginUrl.searchParams.set("error", "account_disabled");
|
|
629
|
+
return context.redirect(loginUrl.toString());
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Set user in locals for use by routes
|
|
633
|
+
locals.user = user;
|
|
634
|
+
} catch (error) {
|
|
635
|
+
console.error("Auth middleware error:", error);
|
|
636
|
+
// On error, redirect to login
|
|
637
|
+
return context.redirect("/_emdash/admin/login");
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return next();
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// =============================================================================
|
|
644
|
+
// Token scope enforcement
|
|
645
|
+
// =============================================================================
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Scope rules: ordered list of (pathPrefix, method, requiredScope) tuples.
|
|
649
|
+
* First matching rule wins. Methods: "*" = any, "WRITE" = POST/PUT/PATCH/DELETE.
|
|
650
|
+
*
|
|
651
|
+
* Routes not matched by any rule default to "admin" scope (fail-closed).
|
|
652
|
+
*/
|
|
653
|
+
const SCOPE_RULES: Array<[prefix: string, method: string, scope: string]> = [
|
|
654
|
+
// Content routes
|
|
655
|
+
["/_emdash/api/content", "GET", "content:read"],
|
|
656
|
+
["/_emdash/api/content", "WRITE", "content:write"],
|
|
657
|
+
|
|
658
|
+
// Media routes (excluding /file/ which is public)
|
|
659
|
+
["/_emdash/api/media/file", "*", "media:read"], // public anyway, but scope if token-authed
|
|
660
|
+
["/_emdash/api/media", "GET", "media:read"],
|
|
661
|
+
["/_emdash/api/media", "WRITE", "media:write"],
|
|
662
|
+
|
|
663
|
+
// Schema routes
|
|
664
|
+
["/_emdash/api/schema", "GET", "schema:read"],
|
|
665
|
+
["/_emdash/api/schema", "WRITE", "schema:write"],
|
|
666
|
+
|
|
667
|
+
// Taxonomy, menu, section, widget, revision — all content domain
|
|
668
|
+
["/_emdash/api/taxonomies", "GET", "content:read"],
|
|
669
|
+
["/_emdash/api/taxonomies", "WRITE", "content:write"],
|
|
670
|
+
["/_emdash/api/menus", "GET", "content:read"],
|
|
671
|
+
["/_emdash/api/menus", "WRITE", "content:write"],
|
|
672
|
+
["/_emdash/api/sections", "GET", "content:read"],
|
|
673
|
+
["/_emdash/api/sections", "WRITE", "content:write"],
|
|
674
|
+
["/_emdash/api/widget-areas", "GET", "content:read"],
|
|
675
|
+
["/_emdash/api/widget-areas", "WRITE", "content:write"],
|
|
676
|
+
["/_emdash/api/revisions", "GET", "content:read"],
|
|
677
|
+
["/_emdash/api/revisions", "WRITE", "content:write"],
|
|
678
|
+
|
|
679
|
+
// Search
|
|
680
|
+
["/_emdash/api/search", "GET", "content:read"],
|
|
681
|
+
["/_emdash/api/search", "WRITE", "admin"],
|
|
682
|
+
|
|
683
|
+
// Import, admin, settings, plugins — all require admin scope
|
|
684
|
+
["/_emdash/api/import", "*", "admin"],
|
|
685
|
+
["/_emdash/api/admin", "*", "admin"],
|
|
686
|
+
["/_emdash/api/settings", "*", "admin"],
|
|
687
|
+
["/_emdash/api/plugins", "*", "admin"],
|
|
688
|
+
|
|
689
|
+
// MCP endpoint — scopes enforced per-tool inside mcp/server.ts
|
|
690
|
+
["/_emdash/api/mcp", "*", "content:read"],
|
|
691
|
+
];
|
|
692
|
+
|
|
693
|
+
const WRITE_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Enforce API token scopes based on the request URL and HTTP method.
|
|
697
|
+
* Returns a 403 Response if the scope is insufficient, or null if allowed.
|
|
698
|
+
*
|
|
699
|
+
* Session-authenticated requests (tokenScopes === undefined) are never checked.
|
|
700
|
+
*/
|
|
701
|
+
function enforceTokenScope(
|
|
702
|
+
pathname: string,
|
|
703
|
+
method: string,
|
|
704
|
+
tokenScopes: string[] | undefined,
|
|
705
|
+
): Response | null {
|
|
706
|
+
// Session auth — implicit full access, no scope restrictions
|
|
707
|
+
if (!tokenScopes) return null;
|
|
708
|
+
|
|
709
|
+
const isWrite = WRITE_METHODS.has(method);
|
|
710
|
+
|
|
711
|
+
for (const [prefix, ruleMethod, scope] of SCOPE_RULES) {
|
|
712
|
+
// Match exact prefix or prefix followed by /
|
|
713
|
+
if (pathname !== prefix && !pathname.startsWith(prefix + "/")) continue;
|
|
714
|
+
|
|
715
|
+
// Check method match
|
|
716
|
+
if (ruleMethod === "*" || (ruleMethod === "WRITE" && isWrite) || ruleMethod === method) {
|
|
717
|
+
if (hasScope(tokenScopes, scope)) return null;
|
|
718
|
+
|
|
719
|
+
return new Response(
|
|
720
|
+
JSON.stringify({
|
|
721
|
+
error: {
|
|
722
|
+
code: "INSUFFICIENT_SCOPE",
|
|
723
|
+
message: `Token lacks required scope: ${scope}`,
|
|
724
|
+
},
|
|
725
|
+
}),
|
|
726
|
+
{ status: 403, headers: { "Content-Type": "application/json", ...MW_CACHE_HEADERS } },
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// No rule matched — default to admin scope (fail-closed)
|
|
732
|
+
if (hasScope(tokenScopes, "admin")) return null;
|
|
733
|
+
|
|
734
|
+
return new Response(
|
|
735
|
+
JSON.stringify({
|
|
736
|
+
error: {
|
|
737
|
+
code: "INSUFFICIENT_SCOPE",
|
|
738
|
+
message: "Token lacks required scope: admin",
|
|
739
|
+
},
|
|
740
|
+
}),
|
|
741
|
+
{ status: 403, headers: { "Content-Type": "application/json", ...MW_CACHE_HEADERS } },
|
|
742
|
+
);
|
|
743
|
+
}
|