emdash 0.7.0 → 0.9.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-Di31kZ28.d.mts → adapters-DoNJiveC.d.mts} +1 -1
- package/dist/{adapters-Di31kZ28.d.mts.map → adapters-DoNJiveC.d.mts.map} +1 -1
- package/dist/{apply-5uslYdUu.mjs → apply-BzltprvY.mjs} +90 -139
- package/dist/apply-BzltprvY.mjs.map +1 -0
- package/dist/astro/index.d.mts +6 -6
- package/dist/astro/index.d.mts.map +1 -1
- package/dist/astro/index.mjs +194 -17
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +6 -7
- package/dist/astro/middleware/auth.d.mts.map +1 -1
- package/dist/astro/middleware/auth.mjs +34 -57
- package/dist/astro/middleware/auth.mjs.map +1 -1
- package/dist/astro/middleware/redirect.d.mts.map +1 -1
- package/dist/astro/middleware/redirect.mjs +17 -12
- package/dist/astro/middleware/redirect.mjs.map +1 -1
- package/dist/astro/middleware/request-context.d.mts.map +1 -1
- package/dist/astro/middleware/request-context.mjs +9 -6
- package/dist/astro/middleware/request-context.mjs.map +1 -1
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +301 -165
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +34 -10
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/{base64-MBPo9ozB.mjs → base64-BRICGH2l.mjs} +1 -1
- package/dist/{base64-MBPo9ozB.mjs.map → base64-BRICGH2l.mjs.map} +1 -1
- package/dist/{byline-C4OVd8b3.mjs → byline-BSaNL1w7.mjs} +5 -5
- package/dist/byline-BSaNL1w7.mjs.map +1 -0
- package/dist/bylines-CvJ3PYz2.mjs +113 -0
- package/dist/bylines-CvJ3PYz2.mjs.map +1 -0
- package/dist/cache-C6N_hhN7.mjs +65 -0
- package/dist/cache-C6N_hhN7.mjs.map +1 -0
- package/dist/{chunks-HGz06Soa.mjs → chunks-NBQVDOci.mjs} +8 -2
- package/dist/{chunks-HGz06Soa.mjs.map → chunks-NBQVDOci.mjs.map} +1 -1
- package/dist/cli/index.mjs +229 -31
- 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 +3 -3
- package/dist/client/index.mjs.map +1 -1
- package/dist/{config-BXwuX8Bx.mjs → config-BI0V3ICQ.mjs} +1 -1
- package/dist/{config-BXwuX8Bx.mjs.map → config-BI0V3ICQ.mjs.map} +1 -1
- package/dist/{content-D7J5y73J.mjs → content-8lOYF0pr.mjs} +43 -28
- package/dist/content-8lOYF0pr.mjs.map +1 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +2 -2
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/libsql.d.mts.map +1 -1
- package/dist/db/libsql.mjs +7 -2
- package/dist/db/libsql.mjs.map +1 -1
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/db/sqlite.d.mts.map +1 -1
- package/dist/db/sqlite.mjs +8 -3
- package/dist/db/sqlite.mjs.map +1 -1
- package/dist/{db-errors-D0UT85nC.mjs → db-errors-WRezodiz.mjs} +1 -1
- package/dist/{db-errors-D0UT85nC.mjs.map → db-errors-WRezodiz.mjs.map} +1 -1
- package/dist/{default-CME5YdZ3.mjs → default-D8ksjWhO.mjs} +1 -1
- package/dist/{default-CME5YdZ3.mjs.map → default-D8ksjWhO.mjs.map} +1 -1
- package/dist/{dialect-helpers-DhTzaUxP.mjs → dialect-helpers-BKCvISIQ.mjs} +19 -2
- package/dist/dialect-helpers-BKCvISIQ.mjs.map +1 -0
- package/dist/{error-CiYn9yDu.mjs → error-D_-tqP-I.mjs} +1 -1
- package/dist/error-D_-tqP-I.mjs.map +1 -0
- package/dist/{index-De6_Xv3v.d.mts → index-BFRaVcD6.d.mts} +243 -40
- package/dist/index-BFRaVcD6.d.mts.map +1 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +29 -25
- package/dist/{load-CBcmDIot.mjs → load-DDqMMvZL.mjs} +2 -2
- package/dist/{load-CBcmDIot.mjs.map → load-DDqMMvZL.mjs.map} +1 -1
- package/dist/{loader-DeiBJEMe.mjs → loader-CKLbBnhK.mjs} +32 -10
- package/dist/loader-CKLbBnhK.mjs.map +1 -0
- package/dist/{manifest-schema-V30qsMft.mjs → manifest-schema-DqWNC3lM.mjs} +45 -3
- package/dist/manifest-schema-DqWNC3lM.mjs.map +1 -0
- package/dist/media/index.d.mts +1 -1
- package/dist/media/index.mjs +1 -1
- package/dist/media/local-runtime.d.mts +7 -7
- package/dist/media/local-runtime.mjs +3 -3
- package/dist/{media-DqHVh136.mjs → media-BW32b4gi.mjs} +4 -7
- package/dist/media-BW32b4gi.mjs.map +1 -0
- package/dist/{mode-CpNnGkPz.mjs → mode-ier8jbBk.mjs} +1 -1
- package/dist/mode-ier8jbBk.mjs.map +1 -0
- package/dist/options-BVp3UsTS.mjs +117 -0
- package/dist/options-BVp3UsTS.mjs.map +1 -0
- package/dist/page/index.d.mts +2 -2
- package/dist/{placeholder-tzpqGWII.d.mts → placeholder-BE4o_2dc.d.mts} +1 -1
- package/dist/{placeholder-tzpqGWII.d.mts.map → placeholder-BE4o_2dc.d.mts.map} +1 -1
- package/dist/{placeholder-C-fk5hYI.mjs → placeholder-CIJejMlK.mjs} +1 -1
- package/dist/placeholder-CIJejMlK.mjs.map +1 -0
- package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
- package/dist/plugins/adapt-sandbox-entry.d.mts.map +1 -1
- package/dist/plugins/adapt-sandbox-entry.mjs +6 -5
- package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -1
- package/dist/public-url-DByxYjUw.mjs +51 -0
- package/dist/public-url-DByxYjUw.mjs.map +1 -0
- package/dist/{query-g4Ug-9j9.mjs → query-Cg9ZKRQ0.mjs} +114 -16
- package/dist/query-Cg9ZKRQ0.mjs.map +1 -0
- package/dist/{redirect-CN0Rt9Ob.mjs → redirect-BhUBKRc1.mjs} +13 -8
- package/dist/redirect-BhUBKRc1.mjs.map +1 -0
- package/dist/{registry-Ci3WxVAr.mjs → registry-Dw70ChxB.mjs} +69 -11
- package/dist/registry-Dw70ChxB.mjs.map +1 -0
- package/dist/{request-cache-DiR961CV.mjs → request-cache-B-bmkipQ.mjs} +1 -1
- package/dist/request-cache-B-bmkipQ.mjs.map +1 -0
- package/dist/runner-Bnoj7vjK.d.mts +44 -0
- package/dist/runner-Bnoj7vjK.d.mts.map +1 -0
- package/dist/{runner-tQ7BJ4T7.mjs → runner-C7ADox5q.mjs} +185 -55
- package/dist/{runner-tQ7BJ4T7.mjs.map → runner-C7ADox5q.mjs.map} +1 -1
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +4 -4
- package/dist/{search-B0effn3j.mjs → search-dOGEccMa.mjs} +341 -152
- package/dist/search-dOGEccMa.mjs.map +1 -0
- package/dist/secrets-CW3reAnU.mjs +314 -0
- package/dist/secrets-CW3reAnU.mjs.map +1 -0
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +15 -14
- package/dist/seo/index.d.mts +1 -1
- package/dist/storage/local.d.mts +1 -1
- package/dist/storage/local.mjs +1 -1
- package/dist/storage/s3.d.mts +1 -1
- package/dist/storage/s3.d.mts.map +1 -1
- package/dist/storage/s3.mjs +4 -4
- package/dist/storage/s3.mjs.map +1 -1
- package/dist/{taxonomies-K2z0Uhnj.mjs → taxonomies-ZlRtD6AG.mjs} +14 -7
- package/dist/taxonomies-ZlRtD6AG.mjs.map +1 -0
- package/dist/{tokens-BFPFx3CA.mjs → tokens-D7zMmWi2.mjs} +2 -2
- package/dist/{tokens-BFPFx3CA.mjs.map → tokens-D7zMmWi2.mjs.map} +1 -1
- package/dist/{transport-BykRfpyy.mjs → transport-BeMCmin1.mjs} +6 -5
- package/dist/{transport-BykRfpyy.mjs.map → transport-BeMCmin1.mjs.map} +1 -1
- package/dist/{transport-H4Iwx7tC.d.mts → transport-DNEfeMaU.d.mts} +1 -1
- package/dist/{transport-H4Iwx7tC.d.mts.map → transport-DNEfeMaU.d.mts.map} +1 -1
- package/dist/types-4fVtCIm0.mjs +68 -0
- package/dist/types-4fVtCIm0.mjs.map +1 -0
- package/dist/{types-CnZYHyLW.d.mts → types-BSyXeCFW.d.mts} +24 -2
- package/dist/{types-CnZYHyLW.d.mts.map → types-BSyXeCFW.d.mts.map} +1 -1
- package/dist/{types-DgrIP0tF.d.mts → types-BuBIptGk.d.mts} +80 -106
- package/dist/types-BuBIptGk.d.mts.map +1 -0
- package/dist/{types-BH2L167P.mjs → types-CDbKp7ND.mjs} +1 -1
- package/dist/{types-BH2L167P.mjs.map → types-CDbKp7ND.mjs.map} +1 -1
- package/dist/{types-DDS4MxsT.mjs → types-CIOg5AR8.mjs} +1 -1
- package/dist/{types-DDS4MxsT.mjs.map → types-CIOg5AR8.mjs.map} +1 -1
- package/dist/{types-6CUZRrZP.d.mts → types-CJsYGpco.d.mts} +24 -2
- package/dist/{types-6CUZRrZP.d.mts.map → types-CJsYGpco.d.mts.map} +1 -1
- package/dist/types-CRxNbK-Z.mjs +68 -0
- package/dist/types-CRxNbK-Z.mjs.map +1 -0
- package/dist/{types-C2v0c34j.d.mts → types-CrtWgIvl.d.mts} +1 -1
- package/dist/{types-C2v0c34j.d.mts.map → types-CrtWgIvl.d.mts.map} +1 -1
- package/dist/{types-CFWjXmus.d.mts → types-M78DQ1lx.d.mts} +1 -1
- package/dist/{types-CFWjXmus.d.mts.map → types-M78DQ1lx.d.mts.map} +1 -1
- package/dist/{validate-CqsNItbt.mjs → validate-Baqf0slj.mjs} +3 -3
- package/dist/{validate-CqsNItbt.mjs.map → validate-Baqf0slj.mjs.map} +1 -1
- package/dist/{validate-kM8Pjuf7.d.mts → validate-BfQh_C_y.d.mts} +4 -4
- package/dist/{validate-kM8Pjuf7.d.mts.map → validate-BfQh_C_y.d.mts.map} +1 -1
- package/dist/validation-BfEI7tNe.mjs +144 -0
- package/dist/validation-BfEI7tNe.mjs.map +1 -0
- package/dist/version-DoxrVdYf.mjs +7 -0
- package/dist/{version-BnTKdfam.mjs.map → version-DoxrVdYf.mjs.map} +1 -1
- package/dist/zod-generator-CC0xNe_K.mjs +132 -0
- package/dist/zod-generator-CC0xNe_K.mjs.map +1 -0
- package/locals.d.ts +1 -6
- package/package.json +21 -7
- package/src/api/auth-storage.ts +37 -0
- package/src/api/error.ts +6 -0
- package/src/api/errors.ts +8 -0
- package/src/api/handlers/comments.ts +19 -4
- package/src/api/handlers/content.ts +151 -4
- package/src/api/handlers/device-flow.ts +5 -0
- package/src/api/handlers/index.ts +2 -0
- package/src/api/handlers/marketplace.ts +11 -4
- package/src/api/handlers/media.ts +8 -1
- package/src/api/handlers/menus.ts +160 -21
- package/src/api/handlers/oauth-authorization.ts +72 -33
- package/src/api/handlers/redirects.ts +16 -3
- package/src/api/handlers/revision.ts +23 -14
- package/src/api/handlers/sections.ts +8 -1
- package/src/api/handlers/taxonomies.ts +131 -22
- package/src/api/handlers/validation.ts +212 -0
- package/src/api/openapi/document.ts +4 -1
- package/src/api/public-url.ts +54 -5
- package/src/api/route-utils.ts +14 -0
- package/src/api/schemas/comments.ts +2 -2
- package/src/api/schemas/common.ts +1 -1
- package/src/api/schemas/content.ts +17 -0
- package/src/api/schemas/sections.ts +3 -3
- package/src/api/schemas/setup.ts +8 -0
- package/src/api/schemas/users.ts +1 -1
- package/src/api/schemas/widgets.ts +12 -10
- package/src/api/setup-complete.ts +40 -0
- package/src/api/types.ts +5 -1
- package/src/astro/integration/index.ts +30 -2
- package/src/astro/integration/routes.ts +28 -0
- package/src/astro/integration/runtime.ts +49 -1
- package/src/astro/integration/virtual-modules.ts +73 -2
- package/src/astro/integration/vite-config.ts +49 -13
- package/src/astro/middleware/auth.ts +34 -6
- package/src/astro/middleware/redirect.ts +29 -16
- package/src/astro/middleware/request-context.ts +15 -5
- package/src/astro/middleware.ts +41 -10
- package/src/astro/routes/PluginRegistry.tsx +10 -1
- package/src/astro/routes/api/auth/invite/complete.ts +6 -1
- package/src/astro/routes/api/auth/mode.ts +57 -0
- package/src/astro/routes/api/auth/oauth/[provider]/callback.ts +23 -3
- package/src/astro/routes/api/auth/oauth/[provider].ts +10 -4
- package/src/astro/routes/api/auth/passkey/register/verify.ts +6 -1
- package/src/astro/routes/api/auth/passkey/verify.ts +6 -1
- package/src/astro/routes/api/auth/signup/complete.ts +6 -1
- package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +2 -2
- package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +4 -2
- package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +34 -12
- package/src/astro/routes/api/content/[collection]/[id]/publish.ts +32 -2
- package/src/astro/routes/api/content/[collection]/[id]/restore.ts +4 -2
- package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +3 -2
- package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +8 -4
- package/src/astro/routes/api/content/[collection]/[id]/translations.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id].ts +12 -0
- package/src/astro/routes/api/content/[collection]/index.ts +1 -9
- package/src/astro/routes/api/import/wordpress/execute.ts +3 -1
- package/src/astro/routes/api/import/wordpress/media.ts +2 -7
- package/src/astro/routes/api/import/wordpress/prepare.ts +9 -0
- package/src/astro/routes/api/import/wordpress-plugin/execute.ts +3 -1
- package/src/astro/routes/api/manifest.ts +62 -45
- package/src/astro/routes/api/media/[id]/confirm.ts +10 -1
- package/src/astro/routes/api/media/providers/[providerId]/index.ts +12 -3
- package/src/astro/routes/api/openapi.json.ts +27 -10
- package/src/astro/routes/api/redirects/404s/index.ts +10 -4
- package/src/astro/routes/api/redirects/404s/summary.ts +4 -2
- package/src/astro/routes/api/redirects/[id].ts +10 -4
- package/src/astro/routes/api/redirects/index.ts +7 -3
- package/src/astro/routes/api/revisions/[revisionId]/index.ts +1 -1
- package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +0 -2
- package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +0 -1
- package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +0 -1
- package/src/astro/routes/api/schema/collections/[slug]/index.ts +2 -2
- package/src/astro/routes/api/schema/collections/index.ts +1 -1
- package/src/astro/routes/api/search/index.ts +10 -2
- package/src/astro/routes/api/sections/[slug].ts +10 -4
- package/src/astro/routes/api/sections/index.ts +7 -3
- package/src/astro/routes/api/settings/email.ts +4 -9
- package/src/astro/routes/api/setup/admin-verify.ts +6 -1
- package/src/astro/routes/api/setup/admin.ts +8 -2
- package/src/astro/routes/api/setup/index.ts +2 -2
- package/src/astro/routes/api/setup/status.ts +3 -1
- package/src/astro/routes/api/snapshot.ts +44 -18
- package/src/astro/routes/api/taxonomies/index.ts +0 -1
- package/src/astro/routes/api/themes/preview.ts +11 -5
- package/src/astro/routes/api/widget-areas/[name]/widgets/[id].ts +4 -1
- package/src/astro/routes/api/widget-areas/[name]/widgets.ts +4 -1
- package/src/astro/routes/api/widget-areas/[name].ts +4 -1
- package/src/astro/routes/api/widget-areas/index.ts +4 -1
- package/src/astro/types.ts +32 -3
- package/src/auth/allowed-origins.ts +168 -0
- package/src/auth/mode.ts +15 -3
- package/src/auth/passkey-config.ts +35 -13
- package/src/auth/providers/github-admin.tsx +29 -0
- package/src/auth/providers/github.ts +31 -0
- package/src/auth/providers/google-admin.tsx +44 -0
- package/src/auth/providers/google.ts +31 -0
- package/src/auth/types.ts +114 -4
- package/src/bylines/index.ts +37 -88
- package/src/cli/commands/auth.ts +28 -6
- package/src/cli/commands/bundle-utils.ts +11 -2
- package/src/cli/commands/bundle.ts +31 -9
- package/src/cli/commands/content.ts +13 -0
- package/src/cli/commands/login.ts +8 -1
- package/src/cli/commands/publish.ts +24 -0
- package/src/cli/commands/secrets.ts +183 -0
- package/src/cli/credentials.ts +1 -1
- package/src/cli/index.ts +5 -1
- package/src/client/index.ts +4 -4
- package/src/client/transport.ts +17 -7
- package/src/components/Break.astro +2 -2
- package/src/components/EmDashHead.astro +18 -13
- package/src/components/EmDashImage.astro +7 -6
- package/src/components/Embed.astro +1 -1
- package/src/components/Gallery.astro +6 -4
- package/src/components/Image.astro +9 -4
- package/src/components/InlinePortableTextEditor.tsx +106 -19
- package/src/components/LiveSearch.astro +5 -14
- package/src/config/secrets.ts +528 -0
- package/src/database/dialect-helpers.ts +50 -0
- package/src/database/migrations/034_published_at_index.ts +1 -1
- package/src/database/migrations/035_bounded_404_log.ts +56 -39
- package/src/database/migrations/runner.ts +156 -23
- package/src/database/repositories/audit.ts +6 -8
- package/src/database/repositories/byline.ts +6 -8
- package/src/database/repositories/comment.ts +12 -16
- package/src/database/repositories/content.ts +76 -52
- package/src/database/repositories/index.ts +1 -1
- package/src/database/repositories/media.ts +10 -13
- package/src/database/repositories/plugin-storage.ts +4 -6
- package/src/database/repositories/redirect.ts +26 -19
- package/src/database/repositories/taxonomy.ts +40 -3
- package/src/database/repositories/types.ts +57 -8
- package/src/database/repositories/user.ts +6 -8
- package/src/db/libsql.ts +1 -3
- package/src/db/sqlite.ts +2 -5
- package/src/emdash-runtime.ts +388 -247
- package/src/index.ts +14 -1
- package/src/loader.ts +30 -6
- package/src/mcp/server.ts +781 -141
- package/src/media/normalize.ts +1 -1
- package/src/media/url.ts +78 -0
- package/src/page/site-identity.ts +58 -0
- package/src/plugins/adapt-sandbox-entry.ts +22 -10
- package/src/plugins/context.ts +13 -10
- package/src/plugins/define-plugin.ts +40 -12
- package/src/plugins/email-console.ts +10 -3
- package/src/plugins/hooks.ts +34 -19
- package/src/plugins/index.ts +9 -0
- package/src/plugins/manifest-schema.ts +49 -2
- package/src/plugins/types.ts +174 -13
- package/src/preview/urls.ts +23 -3
- package/src/query.ts +149 -6
- package/src/redirects/cache.ts +38 -18
- package/src/request-cache.ts +3 -0
- package/src/schema/registry.ts +97 -5
- package/src/schema/zod-generator.ts +27 -5
- package/src/search/fts-manager.ts +0 -2
- package/src/search/query.ts +111 -26
- package/src/search/types.ts +8 -1
- package/src/sections/index.ts +7 -9
- package/src/seed/apply.ts +2 -0
- package/src/settings/index.ts +80 -6
- package/src/settings/types.ts +23 -1
- package/src/storage/s3.ts +12 -6
- package/src/taxonomies/index.ts +11 -1
- package/src/virtual-modules.d.ts +21 -1
- package/src/widgets/index.ts +1 -1
- package/dist/apply-5uslYdUu.mjs.map +0 -1
- package/dist/byline-C4OVd8b3.mjs.map +0 -1
- package/dist/bylines-hPTW79hw.mjs +0 -157
- package/dist/bylines-hPTW79hw.mjs.map +0 -1
- package/dist/cache-BkKBuIvS.mjs +0 -56
- package/dist/cache-BkKBuIvS.mjs.map +0 -1
- package/dist/chunk-ClPoSABd.mjs +0 -21
- package/dist/content-D7J5y73J.mjs.map +0 -1
- package/dist/dialect-helpers-DhTzaUxP.mjs.map +0 -1
- package/dist/error-CiYn9yDu.mjs.map +0 -1
- package/dist/index-De6_Xv3v.d.mts.map +0 -1
- package/dist/loader-DeiBJEMe.mjs.map +0 -1
- package/dist/manifest-schema-V30qsMft.mjs.map +0 -1
- package/dist/media-DqHVh136.mjs.map +0 -1
- package/dist/mode-CpNnGkPz.mjs.map +0 -1
- package/dist/placeholder-C-fk5hYI.mjs.map +0 -1
- package/dist/query-g4Ug-9j9.mjs.map +0 -1
- package/dist/redirect-CN0Rt9Ob.mjs.map +0 -1
- package/dist/registry-Ci3WxVAr.mjs.map +0 -1
- package/dist/request-cache-DiR961CV.mjs.map +0 -1
- package/dist/runner-BR2xKwhn.d.mts +0 -34
- package/dist/runner-BR2xKwhn.d.mts.map +0 -1
- package/dist/search-B0effn3j.mjs.map +0 -1
- package/dist/taxonomies-K2z0Uhnj.mjs.map +0 -1
- package/dist/types-CMMN0pNg.mjs +0 -31
- package/dist/types-CMMN0pNg.mjs.map +0 -1
- package/dist/types-DgrIP0tF.d.mts.map +0 -1
- package/dist/version-BnTKdfam.mjs +0 -7
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
|
|
13
13
|
import { defineMiddleware } from "astro:middleware";
|
|
14
14
|
|
|
15
|
+
import { resolveSecretsCached } from "#config/secrets.js";
|
|
16
|
+
|
|
15
17
|
import { verifyPreviewToken, parseContentId } from "../../preview/tokens.js";
|
|
16
18
|
import { getRequestContext, runWithContext } from "../../request-context.js";
|
|
17
19
|
import { renderToolbar } from "../../visual-editing/toolbar.js";
|
|
@@ -79,17 +81,25 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
79
81
|
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Astro context includes currentLocale when i18n is configured
|
|
80
82
|
const locale = (context as { currentLocale?: string }).currentLocale;
|
|
81
83
|
|
|
82
|
-
// Verify preview token if present
|
|
84
|
+
// Verify preview token if present.
|
|
85
|
+
// The preview secret is resolved via `resolveSecretsCached`: env wins,
|
|
86
|
+
// otherwise a DB-stored value is read (or generated on first need).
|
|
87
|
+
// `emdash.db` is set by the runtime middleware which runs first; the
|
|
88
|
+
// only path where it's missing is a runtime-init failure.
|
|
83
89
|
let preview: { collection: string; id: string } | undefined;
|
|
84
90
|
if (hasPreviewToken) {
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const result = await verifyPreviewToken({ url, secret });
|
|
91
|
+
const db = context.locals.emdash?.db;
|
|
92
|
+
if (db) {
|
|
93
|
+
const { previewSecret } = await resolveSecretsCached(db);
|
|
94
|
+
const result = await verifyPreviewToken({ url, secret: previewSecret });
|
|
89
95
|
if (result.valid) {
|
|
90
96
|
const { collection, id } = parseContentId(result.payload.cid);
|
|
91
97
|
preview = { collection, id };
|
|
92
98
|
}
|
|
99
|
+
} else {
|
|
100
|
+
console.warn(
|
|
101
|
+
"[emdash] Preview token present but EmDash runtime not initialized; preview disabled.",
|
|
102
|
+
);
|
|
93
103
|
}
|
|
94
104
|
}
|
|
95
105
|
|
package/src/astro/middleware.ts
CHANGED
|
@@ -43,8 +43,10 @@ import {
|
|
|
43
43
|
} from "../emdash-runtime.js";
|
|
44
44
|
import { setI18nConfig } from "../i18n/config.js";
|
|
45
45
|
import type { Database, Storage } from "../index.js";
|
|
46
|
+
import { createPublicMediaUrlResolver } from "../media/url.js";
|
|
46
47
|
import type { SandboxRunner } from "../plugins/sandbox/types.js";
|
|
47
48
|
import type { ResolvedPlugin } from "../plugins/types.js";
|
|
49
|
+
import { invalidateUrlPatternCache } from "../query.js";
|
|
48
50
|
import { getRequestContext, runWithContext } from "../request-context.js";
|
|
49
51
|
import type { EmDashConfig } from "./integration/runtime.js";
|
|
50
52
|
import type { EmDashHandlers } from "./types.js";
|
|
@@ -232,6 +234,20 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
232
234
|
const { request, locals, cookies } = context;
|
|
233
235
|
const url = context.url;
|
|
234
236
|
|
|
237
|
+
// Fast path: routes outside /_emdash/ that plugins inject (e.g.,
|
|
238
|
+
// /.well-known/atproto-client-metadata.json) skip the entire runtime
|
|
239
|
+
// init + middleware chain. External servers fetch these with tight
|
|
240
|
+
// timeouts (~1-2s) so they must respond quickly even on cold starts.
|
|
241
|
+
if (!url.pathname.startsWith("/_emdash") && virtualConfig?.authProviders) {
|
|
242
|
+
const isPluginFastRoute = virtualConfig.authProviders.some(
|
|
243
|
+
(p: { routes?: { pattern?: string }[] }) =>
|
|
244
|
+
p.routes?.some((r: { pattern?: string }) => r.pattern && url.pathname === r.pattern),
|
|
245
|
+
);
|
|
246
|
+
if (isPluginFastRoute) {
|
|
247
|
+
return finalizeResponse(await next());
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
235
251
|
const queryRecorder = isInstrumentationEnabled()
|
|
236
252
|
? createRecorder(url.pathname, request.method, request.headers.get("x-perf-phase") ?? "default")
|
|
237
253
|
: undefined;
|
|
@@ -256,8 +272,15 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
256
272
|
// Read the Astro session user once up-front. Both the anonymous fast path
|
|
257
273
|
// and the full doInit path need this, and the session store is network-backed
|
|
258
274
|
// (KV / Durable Object) so we want to avoid re-fetching on the hot path.
|
|
259
|
-
// Skipped entirely for
|
|
260
|
-
|
|
275
|
+
// Skipped entirely for:
|
|
276
|
+
// - prerendered requests (no session at build time)
|
|
277
|
+
// - requests without an `astro-session` cookie (no session to look up)
|
|
278
|
+
// The cookie check matters on Cloudflare Workers, where Astro's session
|
|
279
|
+
// backend is KV: calling session.get() on every anonymous public request
|
|
280
|
+
// turns normal traffic into a flood of KV read misses. See #733.
|
|
281
|
+
const hasSessionCookie = cookies.get("astro-session") !== undefined;
|
|
282
|
+
const sessionUser =
|
|
283
|
+
context.isPrerendered || !hasSessionCookie ? null : await context.session?.get("user");
|
|
261
284
|
|
|
262
285
|
if (!isEmDashRoute && !isPublicRuntimeRoute && !hasEditCookie && !hasPreviewToken) {
|
|
263
286
|
if (!sessionUser && !playgroundDb) {
|
|
@@ -301,10 +324,11 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
301
324
|
try {
|
|
302
325
|
const runtime = await getRuntime(config, initSubTimings);
|
|
303
326
|
setupVerified = true;
|
|
304
|
-
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- partial object; getPageRuntime() only checks for
|
|
327
|
+
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- partial object; getPageRuntime() only checks for the page-contribution methods
|
|
305
328
|
locals.emdash = {
|
|
306
329
|
collectPageMetadata: runtime.collectPageMetadata.bind(runtime),
|
|
307
330
|
collectPageFragments: runtime.collectPageFragments.bind(runtime),
|
|
331
|
+
getPublicMediaUrl: createPublicMediaUrlResolver(runtime.storage),
|
|
308
332
|
} as EmDashHandlers;
|
|
309
333
|
} catch {
|
|
310
334
|
// Non-fatal — EmDashHead will fall back to base SEO contributions
|
|
@@ -378,13 +402,13 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
378
402
|
// Runtime init runs migrations, so the DB is guaranteed set up
|
|
379
403
|
setupVerified = true;
|
|
380
404
|
|
|
381
|
-
//
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
405
|
+
// The manifest is no longer pre-loaded here. It's admin-only
|
|
406
|
+
// content that public/anonymous requests never read, and
|
|
407
|
+
// loading it on every request put logged-out hot paths on
|
|
408
|
+
// the same staleness budget as admin operations. Admin
|
|
409
|
+
// routes call `emdash.getManifest()` directly.
|
|
385
410
|
|
|
386
411
|
// Attach to locals for route handlers
|
|
387
|
-
locals.emdashManifest = manifest;
|
|
388
412
|
locals.emdash = {
|
|
389
413
|
// Content handlers
|
|
390
414
|
handleContentList: runtime.handleContentList.bind(runtime),
|
|
@@ -445,6 +469,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
445
469
|
// Direct access (for advanced use cases)
|
|
446
470
|
storage: runtime.storage,
|
|
447
471
|
db: runtime.db,
|
|
472
|
+
getPublicMediaUrl: createPublicMediaUrlResolver(runtime.storage),
|
|
448
473
|
hooks: runtime.hooks,
|
|
449
474
|
email: runtime.email,
|
|
450
475
|
configuredPlugins: runtime.configuredPlugins,
|
|
@@ -452,8 +477,14 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
452
477
|
// Configuration (for checking database type, auth mode, etc.)
|
|
453
478
|
config,
|
|
454
479
|
|
|
455
|
-
//
|
|
456
|
-
|
|
480
|
+
// Lazy manifest accessor — admin-only consumers call this on
|
|
481
|
+
// demand. `requestCached` inside `getManifest` dedupes within
|
|
482
|
+
// a single request.
|
|
483
|
+
getManifest: runtime.getManifest.bind(runtime),
|
|
484
|
+
|
|
485
|
+
// Clear the URL pattern cache after schema mutations that
|
|
486
|
+
// affect collection URL patterns.
|
|
487
|
+
invalidateUrlPatternCache,
|
|
457
488
|
|
|
458
489
|
// Sandbox runner (for marketplace plugin install/update)
|
|
459
490
|
getSandboxRunner: runtime.getSandboxRunner.bind(runtime),
|
|
@@ -10,6 +10,8 @@ import { AdminApp } from "@emdash-cms/admin";
|
|
|
10
10
|
import type { Messages } from "@lingui/core";
|
|
11
11
|
// @ts-ignore - virtual module generated by integration
|
|
12
12
|
import { pluginAdmins } from "virtual:emdash/admin-registry";
|
|
13
|
+
// @ts-ignore - virtual module generated by integration
|
|
14
|
+
import { authProviders } from "virtual:emdash/auth-providers";
|
|
13
15
|
|
|
14
16
|
interface AdminWrapperProps {
|
|
15
17
|
locale: string;
|
|
@@ -17,5 +19,12 @@ interface AdminWrapperProps {
|
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
export default function AdminWrapper({ locale, messages }: AdminWrapperProps) {
|
|
20
|
-
return
|
|
22
|
+
return (
|
|
23
|
+
<AdminApp
|
|
24
|
+
pluginAdmins={pluginAdmins}
|
|
25
|
+
authProviders={authProviders}
|
|
26
|
+
locale={locale}
|
|
27
|
+
messages={messages}
|
|
28
|
+
/>
|
|
29
|
+
);
|
|
21
30
|
}
|
|
@@ -17,6 +17,7 @@ import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
|
17
17
|
import { isParseError, parseBody } from "#api/parse.js";
|
|
18
18
|
import { getPublicOrigin } from "#api/public-url.js";
|
|
19
19
|
import { inviteCompleteBody } from "#api/schemas.js";
|
|
20
|
+
import { getConfiguredAllowedOrigins, validateAllowedOrigins } from "#auth/allowed-origins.js";
|
|
20
21
|
import { createChallengeStore } from "#auth/challenge-store.js";
|
|
21
22
|
import { getPasskeyConfig } from "#auth/passkey-config.js";
|
|
22
23
|
import { OptionsRepository } from "#db/repositories/options.js";
|
|
@@ -39,7 +40,11 @@ export const POST: APIRoute = async ({ request, locals, session }) => {
|
|
|
39
40
|
const options = new OptionsRepository(emdash.db);
|
|
40
41
|
const siteName = (await options.get<string>("emdash:site_title")) ?? undefined;
|
|
41
42
|
const siteUrl = getPublicOrigin(url, emdash?.config);
|
|
42
|
-
const
|
|
43
|
+
const allowedOrigins = validateAllowedOrigins(
|
|
44
|
+
siteUrl,
|
|
45
|
+
getConfiguredAllowedOrigins(emdash?.config),
|
|
46
|
+
);
|
|
47
|
+
const passkeyConfig = getPasskeyConfig(url, siteName, siteUrl, allowedOrigins);
|
|
43
48
|
|
|
44
49
|
// Verify the passkey registration response
|
|
45
50
|
const challengeStore = createChallengeStore(emdash.db);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /_emdash/api/auth/mode
|
|
3
|
+
*
|
|
4
|
+
* Public endpoint that returns the active authentication mode.
|
|
5
|
+
* Used by the login page to determine which login UI to render.
|
|
6
|
+
*
|
|
7
|
+
* Unlike the full manifest endpoint, this is intentionally public
|
|
8
|
+
* and returns only the auth mode — no collection schemas, plugin
|
|
9
|
+
* info, or other internal details.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { APIRoute } from "astro";
|
|
13
|
+
|
|
14
|
+
import { getAuthMode } from "#auth/mode.js";
|
|
15
|
+
|
|
16
|
+
export const prerender = false;
|
|
17
|
+
|
|
18
|
+
export const GET: APIRoute = async ({ locals }) => {
|
|
19
|
+
const { emdash } = locals;
|
|
20
|
+
|
|
21
|
+
const authMode = getAuthMode(emdash?.config);
|
|
22
|
+
|
|
23
|
+
// Only check signup for passkey auth (external providers handle their own)
|
|
24
|
+
let signupEnabled = false;
|
|
25
|
+
if (emdash?.db && authMode.type === "passkey") {
|
|
26
|
+
try {
|
|
27
|
+
const { sql } = await import("kysely");
|
|
28
|
+
const result = await sql<{ cnt: unknown }>`
|
|
29
|
+
SELECT COUNT(*) as cnt FROM allowed_domains WHERE enabled = 1
|
|
30
|
+
`.execute(emdash.db);
|
|
31
|
+
signupEnabled = Number(result.rows[0]?.cnt ?? 0) > 0;
|
|
32
|
+
} catch {
|
|
33
|
+
// Table may not exist yet
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Collect pluggable auth providers (from authProviders config)
|
|
38
|
+
const providers = (emdash?.config?.authProviders ?? []).map((p) => ({
|
|
39
|
+
id: p.id,
|
|
40
|
+
label: p.label,
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
return Response.json(
|
|
44
|
+
{
|
|
45
|
+
data: {
|
|
46
|
+
authMode: authMode.type === "external" ? authMode.providerType : "passkey",
|
|
47
|
+
signupEnabled,
|
|
48
|
+
providers,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
headers: {
|
|
53
|
+
"Cache-Control": "private, no-store",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
};
|
|
@@ -18,7 +18,9 @@ import {
|
|
|
18
18
|
import { createKyselyAdapter } from "@emdash-cms/auth/adapters/kysely";
|
|
19
19
|
|
|
20
20
|
import { getPublicOrigin } from "#api/public-url.js";
|
|
21
|
+
import { finalizeSetup } from "#api/setup-complete.js";
|
|
21
22
|
import { createOAuthStateStore } from "#auth/oauth-state-store.js";
|
|
23
|
+
import { OptionsRepository } from "#db/repositories/options.js";
|
|
22
24
|
|
|
23
25
|
type ProviderName = "github" | "google";
|
|
24
26
|
|
|
@@ -126,10 +128,22 @@ export const GET: APIRoute = async ({ params, request, locals, session, redirect
|
|
|
126
128
|
);
|
|
127
129
|
}
|
|
128
130
|
|
|
131
|
+
const adapter = createKyselyAdapter(emdash.db);
|
|
132
|
+
const stateStore = createOAuthStateStore(emdash.db);
|
|
133
|
+
|
|
129
134
|
const config: OAuthConsumerConfig = {
|
|
130
135
|
baseUrl: `${getPublicOrigin(url, emdash?.config)}/_emdash`,
|
|
131
136
|
providers,
|
|
132
137
|
canSelfSignup: async (email: string) => {
|
|
138
|
+
// During setup: first user becomes admin.
|
|
139
|
+
// Check setup_complete flag instead of countUsers() to avoid
|
|
140
|
+
// a TOCTOU race where concurrent callbacks both see 0 users.
|
|
141
|
+
const options = new OptionsRepository(emdash.db);
|
|
142
|
+
const setupComplete = await options.get("emdash:setup_complete");
|
|
143
|
+
if (setupComplete !== true && setupComplete !== "true") {
|
|
144
|
+
return { allowed: true, role: Role.ADMIN };
|
|
145
|
+
}
|
|
146
|
+
|
|
133
147
|
// Extract domain from email
|
|
134
148
|
const domain = email.split("@")[1]?.toLowerCase();
|
|
135
149
|
if (!domain) {
|
|
@@ -168,10 +182,16 @@ export const GET: APIRoute = async ({ params, request, locals, session, redirect
|
|
|
168
182
|
},
|
|
169
183
|
};
|
|
170
184
|
|
|
171
|
-
const
|
|
172
|
-
const
|
|
173
|
-
|
|
185
|
+
const options = new OptionsRepository(emdash.db);
|
|
186
|
+
const setupCompleteBefore = await options.get("emdash:setup_complete");
|
|
174
187
|
const user = await handleOAuthCallback(config, adapter, provider, code, state, stateStore);
|
|
188
|
+
const isFirstUser = setupCompleteBefore !== true && setupCompleteBefore !== "true";
|
|
189
|
+
|
|
190
|
+
// Finalize setup outside the transaction (idempotent, safe if two callbacks race).
|
|
191
|
+
if (isFirstUser) {
|
|
192
|
+
await finalizeSetup(emdash.db);
|
|
193
|
+
console.log(`[oauth] Setup complete: created admin user via ${provider} (${user.email})`);
|
|
194
|
+
}
|
|
175
195
|
|
|
176
196
|
// Create session
|
|
177
197
|
if (session) {
|
|
@@ -71,16 +71,22 @@ export const GET: APIRoute = async ({ params, request, locals, redirect }) => {
|
|
|
71
71
|
const { emdash } = locals;
|
|
72
72
|
const provider = params.provider;
|
|
73
73
|
|
|
74
|
+
// Determine where to redirect errors (setup wizard or login page)
|
|
75
|
+
const referer = request.headers.get("referer") ?? "";
|
|
76
|
+
const errorRedirectBase = referer.includes("/setup")
|
|
77
|
+
? "/_emdash/admin/setup"
|
|
78
|
+
: "/_emdash/admin/login";
|
|
79
|
+
|
|
74
80
|
// Validate provider
|
|
75
81
|
if (!provider || !isValidProvider(provider)) {
|
|
76
82
|
return redirect(
|
|
77
|
-
|
|
83
|
+
`${errorRedirectBase}?error=invalid_provider&message=${encodeURIComponent("Invalid OAuth provider")}`,
|
|
78
84
|
);
|
|
79
85
|
}
|
|
80
86
|
|
|
81
87
|
if (!emdash?.db) {
|
|
82
88
|
return redirect(
|
|
83
|
-
|
|
89
|
+
`${errorRedirectBase}?error=server_error&message=${encodeURIComponent("Database not configured")}`,
|
|
84
90
|
);
|
|
85
91
|
}
|
|
86
92
|
|
|
@@ -97,7 +103,7 @@ export const GET: APIRoute = async ({ params, request, locals, redirect }) => {
|
|
|
97
103
|
|
|
98
104
|
if (!providers[provider]) {
|
|
99
105
|
return redirect(
|
|
100
|
-
|
|
106
|
+
`${errorRedirectBase}?error=provider_not_configured&message=${encodeURIComponent(`OAuth provider ${provider} is not configured. Set either EMDASH_OAUTH_${provider.toUpperCase()}_CLIENT_ID and EMDASH_OAUTH_${provider.toUpperCase()}_CLIENT_SECRET, or ${provider.toUpperCase()}_CLIENT_ID and ${provider.toUpperCase()}_CLIENT_SECRET.`)}`,
|
|
101
107
|
);
|
|
102
108
|
}
|
|
103
109
|
|
|
@@ -114,7 +120,7 @@ export const GET: APIRoute = async ({ params, request, locals, redirect }) => {
|
|
|
114
120
|
} catch (error) {
|
|
115
121
|
console.error("OAuth initiation error:", error);
|
|
116
122
|
return redirect(
|
|
117
|
-
|
|
123
|
+
`${errorRedirectBase}?error=oauth_error&message=${encodeURIComponent("Failed to start OAuth flow. Please try again.")}`,
|
|
118
124
|
);
|
|
119
125
|
}
|
|
120
126
|
};
|
|
@@ -15,6 +15,7 @@ import { apiError, apiSuccess } from "#api/error.js";
|
|
|
15
15
|
import { isParseError, parseBody } from "#api/parse.js";
|
|
16
16
|
import { getPublicOrigin } from "#api/public-url.js";
|
|
17
17
|
import { passkeyRegisterVerifyBody } from "#api/schemas.js";
|
|
18
|
+
import { getConfiguredAllowedOrigins, validateAllowedOrigins } from "#auth/allowed-origins.js";
|
|
18
19
|
import { createChallengeStore } from "#auth/challenge-store.js";
|
|
19
20
|
import { getPasskeyConfig } from "#auth/passkey-config.js";
|
|
20
21
|
import { OptionsRepository } from "#db/repositories/options.js";
|
|
@@ -60,7 +61,11 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
60
61
|
const optionsRepo = new OptionsRepository(emdash.db);
|
|
61
62
|
const siteName = (await optionsRepo.get<string>("emdash:site_title")) ?? undefined;
|
|
62
63
|
const siteUrl = getPublicOrigin(url, emdash?.config);
|
|
63
|
-
const
|
|
64
|
+
const allowedOrigins = validateAllowedOrigins(
|
|
65
|
+
siteUrl,
|
|
66
|
+
getConfiguredAllowedOrigins(emdash?.config),
|
|
67
|
+
);
|
|
68
|
+
const passkeyConfig = getPasskeyConfig(url, siteName, siteUrl, allowedOrigins);
|
|
64
69
|
|
|
65
70
|
// Verify the registration response
|
|
66
71
|
const challengeStore = createChallengeStore(emdash.db);
|
|
@@ -15,6 +15,7 @@ import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
|
15
15
|
import { isParseError, parseBody } from "#api/parse.js";
|
|
16
16
|
import { getPublicOrigin } from "#api/public-url.js";
|
|
17
17
|
import { passkeyVerifyBody } from "#api/schemas.js";
|
|
18
|
+
import { getConfiguredAllowedOrigins, validateAllowedOrigins } from "#auth/allowed-origins.js";
|
|
18
19
|
import { createChallengeStore } from "#auth/challenge-store.js";
|
|
19
20
|
import { getPasskeyConfig } from "#auth/passkey-config.js";
|
|
20
21
|
import { OptionsRepository } from "#db/repositories/options.js";
|
|
@@ -35,7 +36,11 @@ export const POST: APIRoute = async ({ request, locals, session }) => {
|
|
|
35
36
|
const options = new OptionsRepository(emdash.db);
|
|
36
37
|
const siteName = (await options.get<string>("emdash:site_title")) ?? undefined;
|
|
37
38
|
const siteUrl = getPublicOrigin(url, emdash?.config);
|
|
38
|
-
const
|
|
39
|
+
const allowedOrigins = validateAllowedOrigins(
|
|
40
|
+
siteUrl,
|
|
41
|
+
getConfiguredAllowedOrigins(emdash?.config),
|
|
42
|
+
);
|
|
43
|
+
const passkeyConfig = getPasskeyConfig(url, siteName, siteUrl, allowedOrigins);
|
|
39
44
|
|
|
40
45
|
// Authenticate with passkey
|
|
41
46
|
const adapter = createKyselyAdapter(emdash.db);
|
|
@@ -17,6 +17,7 @@ import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
|
17
17
|
import { isParseError, parseBody } from "#api/parse.js";
|
|
18
18
|
import { getPublicOrigin } from "#api/public-url.js";
|
|
19
19
|
import { signupCompleteBody } from "#api/schemas.js";
|
|
20
|
+
import { getConfiguredAllowedOrigins, validateAllowedOrigins } from "#auth/allowed-origins.js";
|
|
20
21
|
import { createChallengeStore } from "#auth/challenge-store.js";
|
|
21
22
|
import { getPasskeyConfig } from "#auth/passkey-config.js";
|
|
22
23
|
import { OptionsRepository } from "#db/repositories/options.js";
|
|
@@ -39,7 +40,11 @@ export const POST: APIRoute = async ({ request, locals, session }) => {
|
|
|
39
40
|
const options = new OptionsRepository(emdash.db);
|
|
40
41
|
const siteName = (await options.get<string>("emdash:site_title")) ?? undefined;
|
|
41
42
|
const siteUrl = getPublicOrigin(url, emdash?.config);
|
|
42
|
-
const
|
|
43
|
+
const allowedOrigins = validateAllowedOrigins(
|
|
44
|
+
siteUrl,
|
|
45
|
+
getConfiguredAllowedOrigins(emdash?.config),
|
|
46
|
+
);
|
|
47
|
+
const passkeyConfig = getPasskeyConfig(url, siteName, siteUrl, allowedOrigins);
|
|
43
48
|
|
|
44
49
|
// Verify the passkey registration response
|
|
45
50
|
const challengeStore = createChallengeStore(emdash.db);
|
|
@@ -14,6 +14,7 @@ import { createCommentBody } from "#api/schemas.js";
|
|
|
14
14
|
import { getSiteBaseUrl } from "#api/site-url.js";
|
|
15
15
|
import { sendCommentNotification } from "#comments/notifications.js";
|
|
16
16
|
import { createComment, type CommentHookRunner } from "#comments/service.js";
|
|
17
|
+
import { resolveSecretsCached } from "#config/secrets.js";
|
|
17
18
|
import { CommentRepository } from "#db/repositories/comment.js";
|
|
18
19
|
import { validateIdentifier } from "#db/validate.js";
|
|
19
20
|
import { extractRequestMeta } from "#plugins/request-meta.js";
|
|
@@ -140,8 +141,7 @@ export const POST: APIRoute = async ({ params, request, locals }) => {
|
|
|
140
141
|
|
|
141
142
|
// Anti-spam: Rate limiting
|
|
142
143
|
const meta = extractRequestMeta(request, emdash.config);
|
|
143
|
-
const ipSalt =
|
|
144
|
-
import.meta.env.EMDASH_AUTH_SECRET || import.meta.env.AUTH_SECRET || "emdash-ip-salt";
|
|
144
|
+
const { ipSalt } = await resolveSecretsCached(emdash.db);
|
|
145
145
|
let ipHash: string;
|
|
146
146
|
if (meta.ip) {
|
|
147
147
|
ipHash = await hashIp(meta.ip, ipSalt);
|
|
@@ -44,11 +44,13 @@ export const POST: APIRoute = async ({ params, locals, cache }) => {
|
|
|
44
44
|
const denied = requireOwnerPerm(user, authorId, "content:edit_own", "content:edit_any");
|
|
45
45
|
if (denied) return denied;
|
|
46
46
|
|
|
47
|
-
const
|
|
47
|
+
const resolvedId = typeof existingItem?.id === "string" ? existingItem.id : id;
|
|
48
|
+
|
|
49
|
+
const result = await emdash.handleContentDiscardDraft(collection, resolvedId);
|
|
48
50
|
|
|
49
51
|
if (!result.success) return unwrapResult(result);
|
|
50
52
|
|
|
51
|
-
if (cache.enabled) await cache.invalidate({ tags: [collection,
|
|
53
|
+
if (cache.enabled) await cache.invalidate({ tags: [collection, resolvedId] });
|
|
52
54
|
|
|
53
55
|
return unwrapResult(result);
|
|
54
56
|
};
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Request body:
|
|
7
7
|
* {
|
|
8
8
|
* expiresIn?: string | number; // Default: "1h"
|
|
9
|
-
* pathPattern?: string; // Default: "/{collection}/{id}"
|
|
9
|
+
* pathPattern?: string; // Default: "/{collection}/{id}" (or EMDASH_PREVIEW_PATH_PATTERN)
|
|
10
10
|
* }
|
|
11
11
|
*
|
|
12
12
|
* Response:
|
|
@@ -22,8 +22,11 @@ import { requirePerm } from "#api/authorize.js";
|
|
|
22
22
|
import { apiError, apiSuccess, handleError, unwrapResult } from "#api/error.js";
|
|
23
23
|
import { parseOptionalBody, isParseError } from "#api/parse.js";
|
|
24
24
|
import { contentPreviewUrlBody } from "#api/schemas.js";
|
|
25
|
+
import { resolveSecretsCached } from "#config/secrets.js";
|
|
25
26
|
import { getPreviewUrl } from "#preview/index.js";
|
|
26
27
|
|
|
28
|
+
import { getI18nConfig } from "../../../../../../i18n/config.js";
|
|
29
|
+
|
|
27
30
|
export const prerender = false;
|
|
28
31
|
|
|
29
32
|
const DURATION_PATTERN = /^(\d+)([smhdw])$/;
|
|
@@ -35,21 +38,23 @@ export const POST: APIRoute = async ({ params, request, locals }) => {
|
|
|
35
38
|
const collection = params.collection!;
|
|
36
39
|
const id = params.id!;
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (!previewSecret) {
|
|
42
|
-
return apiError(
|
|
43
|
-
"NOT_CONFIGURED",
|
|
44
|
-
"Preview not configured. Set EMDASH_PREVIEW_SECRET environment variable.",
|
|
45
|
-
500,
|
|
46
|
-
);
|
|
41
|
+
if (!emdash?.db) {
|
|
42
|
+
return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500);
|
|
47
43
|
}
|
|
48
44
|
|
|
49
|
-
//
|
|
45
|
+
// Resolve the preview secret. Env override wins; otherwise a stable
|
|
46
|
+
// site-specific value is read from (or generated into) the options table.
|
|
47
|
+
// The resolver always returns a usable secret, so this path can no
|
|
48
|
+
// longer be silently disabled by a missing env var.
|
|
49
|
+
const { previewSecret } = await resolveSecretsCached(emdash.db);
|
|
50
|
+
|
|
51
|
+
// Verify the content exists. The fetched item also yields the entry's
|
|
52
|
+
// locale, used below to resolve the `{locale}` placeholder.
|
|
53
|
+
let entryLocale: string | null = null;
|
|
50
54
|
if (emdash?.handleContentGet) {
|
|
51
55
|
const result = await emdash.handleContentGet(collection, id);
|
|
52
56
|
if (!result.success) return unwrapResult(result);
|
|
57
|
+
entryLocale = result.data?.item?.locale ?? null;
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
// Parse request body
|
|
@@ -57,7 +62,23 @@ export const POST: APIRoute = async ({ params, request, locals }) => {
|
|
|
57
62
|
if (isParseError(body)) return body;
|
|
58
63
|
|
|
59
64
|
const expiresIn = body.expiresIn || "1h";
|
|
60
|
-
|
|
65
|
+
// Allow a project-wide default `pathPattern` so the admin's "View on site"
|
|
66
|
+
// link can match the site's actual route shape without each call having
|
|
67
|
+
// to override the default `/{collection}/{id}`.
|
|
68
|
+
const defaultPathPattern = import.meta.env.EMDASH_PREVIEW_PATH_PATTERN || "/{collection}/{id}";
|
|
69
|
+
const pathPattern = body.pathPattern || defaultPathPattern;
|
|
70
|
+
|
|
71
|
+
// Resolve the locale segment substituted for `{locale}`: empty when the
|
|
72
|
+
// entry is in the default locale and `prefixDefaultLocale` is `false`,
|
|
73
|
+
// the entry's own locale otherwise.
|
|
74
|
+
const i18n = getI18nConfig();
|
|
75
|
+
let localeSegment = "";
|
|
76
|
+
if (entryLocale && i18n) {
|
|
77
|
+
const isDefault = entryLocale === i18n.defaultLocale;
|
|
78
|
+
localeSegment = isDefault && !i18n.prefixDefaultLocale ? "" : entryLocale;
|
|
79
|
+
} else if (entryLocale) {
|
|
80
|
+
localeSegment = entryLocale;
|
|
81
|
+
}
|
|
61
82
|
|
|
62
83
|
// Calculate expiry timestamp
|
|
63
84
|
const expiresInSeconds = typeof expiresIn === "number" ? expiresIn : parseExpiresIn(expiresIn);
|
|
@@ -70,6 +91,7 @@ export const POST: APIRoute = async ({ params, request, locals }) => {
|
|
|
70
91
|
secret: previewSecret,
|
|
71
92
|
expiresIn,
|
|
72
93
|
pathPattern,
|
|
94
|
+
locale: localeSegment,
|
|
73
95
|
});
|
|
74
96
|
|
|
75
97
|
return apiSuccess({ url, expiresAt });
|
|
@@ -2,16 +2,25 @@
|
|
|
2
2
|
* Publish content - promotes draft to live
|
|
3
3
|
*
|
|
4
4
|
* POST /_emdash/api/content/{collection}/{id}/publish
|
|
5
|
+
*
|
|
6
|
+
* Optional JSON body: { publishedAt?: string }
|
|
7
|
+
* publishedAt — ISO 8601 datetime to backdate the publish (e.g. when
|
|
8
|
+
* migrating content). Writing publishedAt requires content:publish_any.
|
|
9
|
+
* Without it, the existing published_at is preserved on re-publish and
|
|
10
|
+
* falls back to the current time on first publish.
|
|
5
11
|
*/
|
|
6
12
|
|
|
13
|
+
import { hasPermission } from "@emdash-cms/auth";
|
|
7
14
|
import type { APIRoute } from "astro";
|
|
8
15
|
|
|
9
16
|
import { requireOwnerPerm } from "#api/authorize.js";
|
|
10
17
|
import { apiError, mapErrorStatus, unwrapResult } from "#api/error.js";
|
|
18
|
+
import { isParseError, parseOptionalBody } from "#api/parse.js";
|
|
19
|
+
import { contentPublishBody } from "#api/schemas.js";
|
|
11
20
|
|
|
12
21
|
export const prerender = false;
|
|
13
22
|
|
|
14
|
-
export const POST: APIRoute = async ({ params, locals, cache }) => {
|
|
23
|
+
export const POST: APIRoute = async ({ params, request, locals, cache }) => {
|
|
15
24
|
const { emdash, user } = locals;
|
|
16
25
|
const collection = params.collection!;
|
|
17
26
|
const id = params.id!;
|
|
@@ -20,6 +29,11 @@ export const POST: APIRoute = async ({ params, locals, cache }) => {
|
|
|
20
29
|
return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500);
|
|
21
30
|
}
|
|
22
31
|
|
|
32
|
+
// Body is optional — empty body means use the legacy behavior (preserve
|
|
33
|
+
// or default published_at). Pass `publishedAt` to backdate.
|
|
34
|
+
const body = await parseOptionalBody(request, contentPublishBody, {});
|
|
35
|
+
if (isParseError(body)) return body;
|
|
36
|
+
|
|
23
37
|
// Fetch item to check ownership
|
|
24
38
|
const existing = await emdash.handleContentGet(collection, id);
|
|
25
39
|
if (!existing.success) {
|
|
@@ -44,9 +58,25 @@ export const POST: APIRoute = async ({ params, locals, cache }) => {
|
|
|
44
58
|
const denied = requireOwnerPerm(user, authorId, "content:publish_own", "content:publish_any");
|
|
45
59
|
if (denied) return denied;
|
|
46
60
|
|
|
61
|
+
// Schema narrows `publishedAt` to `string | undefined`; null is rejected
|
|
62
|
+
// at the schema layer (publish has no semantic meaning for "clear").
|
|
63
|
+
const publishedAt = body?.publishedAt;
|
|
64
|
+
|
|
65
|
+
// Backdating overwrites historical record — gate behind publish_any
|
|
66
|
+
// regardless of ownership.
|
|
67
|
+
if (publishedAt !== undefined && !hasPermission(user, "content:publish_any")) {
|
|
68
|
+
return apiError(
|
|
69
|
+
"FORBIDDEN",
|
|
70
|
+
"Setting publishedAt requires content:publish_any permission",
|
|
71
|
+
403,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
47
75
|
const resolvedId = typeof existingItem?.id === "string" ? existingItem.id : id;
|
|
48
76
|
|
|
49
|
-
const result = await emdash.handleContentPublish(collection, resolvedId
|
|
77
|
+
const result = await emdash.handleContentPublish(collection, resolvedId, {
|
|
78
|
+
publishedAt,
|
|
79
|
+
});
|
|
50
80
|
|
|
51
81
|
if (!result.success) return unwrapResult(result);
|
|
52
82
|
|
|
@@ -44,11 +44,13 @@ export const POST: APIRoute = async ({ params, locals, cache }) => {
|
|
|
44
44
|
const denied = requireOwnerPerm(user, authorId, "content:edit_own", "content:edit_any");
|
|
45
45
|
if (denied) return denied;
|
|
46
46
|
|
|
47
|
-
const
|
|
47
|
+
const resolvedId = typeof existingItem?.id === "string" ? existingItem.id : id;
|
|
48
|
+
|
|
49
|
+
const result = await emdash.handleContentRestore(collection, resolvedId);
|
|
48
50
|
|
|
49
51
|
if (!result.success) return unwrapResult(result);
|
|
50
52
|
|
|
51
|
-
if (cache.enabled) await cache.invalidate({ tags: [collection,
|
|
53
|
+
if (cache.enabled) await cache.invalidate({ tags: [collection, resolvedId] });
|
|
52
54
|
|
|
53
55
|
return unwrapResult(result);
|
|
54
56
|
};
|
|
@@ -22,9 +22,10 @@ export const GET: APIRoute = async ({ params, url, locals }) => {
|
|
|
22
22
|
return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
const
|
|
25
|
+
const limitParam = url.searchParams.get("limit");
|
|
26
|
+
const parsedLimit = limitParam ? parseInt(limitParam, 10) : undefined;
|
|
26
27
|
const result = await emdash.handleRevisionList(collection, id, {
|
|
27
|
-
limit:
|
|
28
|
+
limit: parsedLimit ? Math.max(1, Math.min(parsedLimit, 100)) : undefined,
|
|
28
29
|
});
|
|
29
30
|
|
|
30
31
|
return unwrapResult(result);
|