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
package/src/emdash-runtime.ts
CHANGED
|
@@ -39,13 +39,26 @@ import type {
|
|
|
39
39
|
PageMetadataContribution,
|
|
40
40
|
PageFragmentContribution,
|
|
41
41
|
} from "./plugins/types.js";
|
|
42
|
-
import { invalidateUrlPatternCache } from "./query.js";
|
|
43
42
|
import type { FieldType } from "./schema/types.js";
|
|
44
43
|
import { hashString } from "./utils/hash.js";
|
|
45
44
|
import { COMMIT, VERSION } from "./version.js";
|
|
46
45
|
|
|
47
46
|
const LEADING_SLASH_PATTERN = /^\//;
|
|
48
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Parse a JSON column expected to contain an array of strings.
|
|
50
|
+
*
|
|
51
|
+
* Throws on malformed JSON rather than returning []; callers are responsible
|
|
52
|
+
* for deciding how to handle/log the error. Empty string / null inputs return
|
|
53
|
+
* [] (they represent "no value"). Non-string array entries are filtered out.
|
|
54
|
+
*/
|
|
55
|
+
function parseStringArray(raw: string | null | undefined): string[] {
|
|
56
|
+
if (!raw) return [];
|
|
57
|
+
const parsed: unknown = JSON.parse(raw);
|
|
58
|
+
if (!Array.isArray(parsed)) return [];
|
|
59
|
+
return parsed.filter((v): v is string => typeof v === "string");
|
|
60
|
+
}
|
|
61
|
+
|
|
49
62
|
/** Combined result from a single-pass page contribution collection */
|
|
50
63
|
interface PageContributions {
|
|
51
64
|
metadata: PageMetadataContribution[];
|
|
@@ -97,6 +110,7 @@ import {
|
|
|
97
110
|
DEFAULT_COMMENT_MODERATOR_PLUGIN_ID,
|
|
98
111
|
defaultCommentModerate,
|
|
99
112
|
} from "./comments/moderator.js";
|
|
113
|
+
import { validateEncryptionKeyAtStartup } from "./config/secrets.js";
|
|
100
114
|
import { OptionsRepository } from "./database/repositories/options.js";
|
|
101
115
|
import {
|
|
102
116
|
handleContentList,
|
|
@@ -147,6 +161,7 @@ import { NodeCronScheduler } from "./plugins/scheduler/node.js";
|
|
|
147
161
|
import { PiggybackScheduler } from "./plugins/scheduler/piggyback.js";
|
|
148
162
|
import type { CronScheduler } from "./plugins/scheduler/types.js";
|
|
149
163
|
import { PluginStateRepository } from "./plugins/state.js";
|
|
164
|
+
import { requestCached } from "./request-cache.js";
|
|
150
165
|
import { getRequestContext } from "./request-context.js";
|
|
151
166
|
import { FTSManager } from "./search/fts-manager.js";
|
|
152
167
|
|
|
@@ -237,6 +252,44 @@ export interface RuntimeDependencies {
|
|
|
237
252
|
createSandboxRunner: ((opts: { db: Kysely<Database> }) => SandboxRunner) | null;
|
|
238
253
|
}
|
|
239
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Constructor parameters for `EmDashRuntime`.
|
|
257
|
+
*
|
|
258
|
+
* Production code should use `EmDashRuntime.create()` which discovers and
|
|
259
|
+
* loads all parts (database, plugins, hooks, cron, etc.) and then calls the
|
|
260
|
+
* constructor. Direct construction is supported for callers that already
|
|
261
|
+
* have all the dependencies in hand — for example, integration tests that
|
|
262
|
+
* supply a pre-migrated database and an empty plugin set.
|
|
263
|
+
*
|
|
264
|
+
* Every field corresponds 1:1 to internal state set on the runtime — none of
|
|
265
|
+
* these are derived. If you don't have a value for one, see what `create()`
|
|
266
|
+
* passes for that field as the canonical default.
|
|
267
|
+
*/
|
|
268
|
+
export interface EmDashRuntimeParts {
|
|
269
|
+
db: Kysely<Database>;
|
|
270
|
+
storage: Storage | null;
|
|
271
|
+
configuredPlugins: ResolvedPlugin[];
|
|
272
|
+
sandboxedPlugins: Map<string, SandboxedPlugin>;
|
|
273
|
+
sandboxedPluginEntries: SandboxedPluginEntry[];
|
|
274
|
+
hooks: HookPipeline;
|
|
275
|
+
enabledPlugins: Set<string>;
|
|
276
|
+
pluginStates: Map<string, string>;
|
|
277
|
+
config: EmDashConfig;
|
|
278
|
+
mediaProviders: Map<string, MediaProvider>;
|
|
279
|
+
mediaProviderEntries: MediaProviderEntry[];
|
|
280
|
+
cronExecutor: CronExecutor | null;
|
|
281
|
+
cronScheduler: CronScheduler | null;
|
|
282
|
+
emailPipeline: EmailPipeline | null;
|
|
283
|
+
allPipelinePlugins: ResolvedPlugin[];
|
|
284
|
+
pipelineFactoryOptions: {
|
|
285
|
+
db: Kysely<Database>;
|
|
286
|
+
storage?: Storage;
|
|
287
|
+
siteInfo?: { siteName?: string; siteUrl?: string; locale?: string };
|
|
288
|
+
};
|
|
289
|
+
runtimeDeps: RuntimeDependencies;
|
|
290
|
+
pipelineRef: { current: HookPipeline };
|
|
291
|
+
}
|
|
292
|
+
|
|
240
293
|
/**
|
|
241
294
|
* Convert a ContentItem to Record<string, unknown> for hook consumption.
|
|
242
295
|
* Hooks receive the full item as a flat record.
|
|
@@ -290,10 +343,6 @@ export class EmDashRuntime {
|
|
|
290
343
|
private enabledPlugins: Set<string>;
|
|
291
344
|
private pluginStates: Map<string, string>;
|
|
292
345
|
|
|
293
|
-
private _cachedManifest: EmDashManifest | null = null;
|
|
294
|
-
private _manifestPromise: Promise<EmDashManifest> | null = null;
|
|
295
|
-
private readonly _manifestCacheKey: string;
|
|
296
|
-
|
|
297
346
|
/**
|
|
298
347
|
* Set to true after FTS indexes have been verified for this worker
|
|
299
348
|
* lifetime so we don't re-scan on every admin request. See
|
|
@@ -337,51 +386,26 @@ export class EmDashRuntime {
|
|
|
337
386
|
return this._db;
|
|
338
387
|
}
|
|
339
388
|
|
|
340
|
-
|
|
341
|
-
db
|
|
342
|
-
storage
|
|
343
|
-
configuredPlugins
|
|
344
|
-
sandboxedPlugins
|
|
345
|
-
sandboxedPluginEntries
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
},
|
|
361
|
-
runtimeDeps: RuntimeDependencies,
|
|
362
|
-
pipelineRef: { current: HookPipeline },
|
|
363
|
-
manifestCacheKey: string,
|
|
364
|
-
) {
|
|
365
|
-
this._db = db;
|
|
366
|
-
this.storage = storage;
|
|
367
|
-
this.configuredPlugins = configuredPlugins;
|
|
368
|
-
this.sandboxedPlugins = sandboxedPlugins;
|
|
369
|
-
this.sandboxedPluginEntries = sandboxedPluginEntries;
|
|
370
|
-
this.schemaRegistry = new SchemaRegistry(db);
|
|
371
|
-
this._hooks = hooks;
|
|
372
|
-
this.enabledPlugins = enabledPlugins;
|
|
373
|
-
this.pluginStates = pluginStates;
|
|
374
|
-
this.config = config;
|
|
375
|
-
this.mediaProviders = mediaProviders;
|
|
376
|
-
this.mediaProviderEntries = mediaProviderEntries;
|
|
377
|
-
this.cronExecutor = cronExecutor;
|
|
378
|
-
this.cronScheduler = cronScheduler;
|
|
379
|
-
this.email = emailPipeline;
|
|
380
|
-
this.allPipelinePlugins = allPipelinePlugins;
|
|
381
|
-
this.pipelineFactoryOptions = pipelineFactoryOptions;
|
|
382
|
-
this.runtimeDeps = runtimeDeps;
|
|
383
|
-
this.pipelineRef = pipelineRef;
|
|
384
|
-
this._manifestCacheKey = manifestCacheKey;
|
|
389
|
+
constructor(parts: EmDashRuntimeParts) {
|
|
390
|
+
this._db = parts.db;
|
|
391
|
+
this.storage = parts.storage;
|
|
392
|
+
this.configuredPlugins = parts.configuredPlugins;
|
|
393
|
+
this.sandboxedPlugins = parts.sandboxedPlugins;
|
|
394
|
+
this.sandboxedPluginEntries = parts.sandboxedPluginEntries;
|
|
395
|
+
this.schemaRegistry = new SchemaRegistry(parts.db);
|
|
396
|
+
this._hooks = parts.hooks;
|
|
397
|
+
this.enabledPlugins = parts.enabledPlugins;
|
|
398
|
+
this.pluginStates = parts.pluginStates;
|
|
399
|
+
this.config = parts.config;
|
|
400
|
+
this.mediaProviders = parts.mediaProviders;
|
|
401
|
+
this.mediaProviderEntries = parts.mediaProviderEntries;
|
|
402
|
+
this.cronExecutor = parts.cronExecutor;
|
|
403
|
+
this.cronScheduler = parts.cronScheduler;
|
|
404
|
+
this.email = parts.emailPipeline;
|
|
405
|
+
this.allPipelinePlugins = parts.allPipelinePlugins;
|
|
406
|
+
this.pipelineFactoryOptions = parts.pipelineFactoryOptions;
|
|
407
|
+
this.runtimeDeps = parts.runtimeDeps;
|
|
408
|
+
this.pipelineRef = parts.pipelineRef;
|
|
385
409
|
}
|
|
386
410
|
|
|
387
411
|
/**
|
|
@@ -431,7 +455,6 @@ export class EmDashRuntime {
|
|
|
431
455
|
this.enabledPlugins.delete(pluginId);
|
|
432
456
|
await this.rebuildHookPipeline();
|
|
433
457
|
}
|
|
434
|
-
this.invalidateManifest();
|
|
435
458
|
}
|
|
436
459
|
|
|
437
460
|
/**
|
|
@@ -605,6 +628,13 @@ export class EmDashRuntime {
|
|
|
605
628
|
// Initialize database (connects, runs migrations if needed)
|
|
606
629
|
const db = await phase("rt.db", "DB init + migrations", () => EmDashRuntime.getDatabase(deps));
|
|
607
630
|
|
|
631
|
+
// Validate EMDASH_ENCRYPTION_KEY once here so a malformed value
|
|
632
|
+
// surfaces in startup logs instead of as request-time 500s. The key
|
|
633
|
+
// itself is not yet consumed (a follow-up PR adds plugin-secret
|
|
634
|
+
// encryption); validating early just guards against silent
|
|
635
|
+
// misconfiguration.
|
|
636
|
+
await phase("rt.secrets", "Validate encryption key", () => validateEncryptionKeyAtStartup());
|
|
637
|
+
|
|
608
638
|
// FTS verify/repair is deferred off the cold-start hot path.
|
|
609
639
|
// See EmDashRuntime.ensureSearchHealthy().
|
|
610
640
|
|
|
@@ -668,7 +698,7 @@ export class EmDashRuntime {
|
|
|
668
698
|
const devConsolePlugin = definePlugin({
|
|
669
699
|
id: DEV_CONSOLE_EMAIL_PLUGIN_ID,
|
|
670
700
|
version: "0.0.0",
|
|
671
|
-
capabilities: ["email:
|
|
701
|
+
capabilities: ["hooks.email-transport:register"],
|
|
672
702
|
hooks: {
|
|
673
703
|
"email:deliver": {
|
|
674
704
|
exclusive: true,
|
|
@@ -691,7 +721,7 @@ export class EmDashRuntime {
|
|
|
691
721
|
const defaultModeratorPlugin = definePlugin({
|
|
692
722
|
id: DEFAULT_COMMENT_MODERATOR_PLUGIN_ID,
|
|
693
723
|
version: "0.0.0",
|
|
694
|
-
capabilities: ["read
|
|
724
|
+
capabilities: ["users:read"],
|
|
695
725
|
hooks: {
|
|
696
726
|
"comment:moderate": {
|
|
697
727
|
exclusive: true,
|
|
@@ -842,32 +872,16 @@ export class EmDashRuntime {
|
|
|
842
872
|
}
|
|
843
873
|
});
|
|
844
874
|
|
|
845
|
-
|
|
846
|
-
// COMMIT captures emdash code changes; plugin IDs/versions and i18n
|
|
847
|
-
// capture user astro.config changes (e.g. upgrading a plugin package).
|
|
848
|
-
// DB-driven changes (collections, fields, plugin toggle) go through
|
|
849
|
-
// invalidateManifest(). Sorted for stability across nondeterministic
|
|
850
|
-
// plugin ordering.
|
|
851
|
-
const manifestCacheKey = await hashString(
|
|
852
|
-
[
|
|
853
|
-
COMMIT,
|
|
854
|
-
...deps.plugins.map((p) => `${p.id}@${p.version ?? ""}`).toSorted(),
|
|
855
|
-
...deps.sandboxedPluginEntries.map((e) => `${e.id}@${e.version}`).toSorted(),
|
|
856
|
-
virtualConfig?.i18n?.defaultLocale ?? "",
|
|
857
|
-
(virtualConfig?.i18n?.locales ?? []).toSorted().join(","),
|
|
858
|
-
].join("|"),
|
|
859
|
-
);
|
|
860
|
-
|
|
861
|
-
return new EmDashRuntime(
|
|
875
|
+
return new EmDashRuntime({
|
|
862
876
|
db,
|
|
863
877
|
storage,
|
|
864
|
-
deps.plugins,
|
|
878
|
+
configuredPlugins: deps.plugins,
|
|
865
879
|
sandboxedPlugins,
|
|
866
|
-
deps.sandboxedPluginEntries,
|
|
867
|
-
pipeline,
|
|
880
|
+
sandboxedPluginEntries: deps.sandboxedPluginEntries,
|
|
881
|
+
hooks: pipeline,
|
|
868
882
|
enabledPlugins,
|
|
869
883
|
pluginStates,
|
|
870
|
-
deps.config,
|
|
884
|
+
config: deps.config,
|
|
871
885
|
mediaProviders,
|
|
872
886
|
mediaProviderEntries,
|
|
873
887
|
cronExecutor,
|
|
@@ -875,10 +889,9 @@ export class EmDashRuntime {
|
|
|
875
889
|
emailPipeline,
|
|
876
890
|
allPipelinePlugins,
|
|
877
891
|
pipelineFactoryOptions,
|
|
878
|
-
deps,
|
|
892
|
+
runtimeDeps: deps,
|
|
879
893
|
pipelineRef,
|
|
880
|
-
|
|
881
|
-
);
|
|
894
|
+
});
|
|
882
895
|
}
|
|
883
896
|
|
|
884
897
|
/**
|
|
@@ -954,18 +967,15 @@ export class EmDashRuntime {
|
|
|
954
967
|
const dialect = deps.createDialect(dbConfig.config);
|
|
955
968
|
const db = new Kysely<Database>({ dialect, log: kyselyLogOption() });
|
|
956
969
|
|
|
957
|
-
|
|
970
|
+
await runMigrations(db);
|
|
958
971
|
|
|
959
|
-
//
|
|
960
|
-
//
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
// Non-fatal
|
|
967
|
-
}
|
|
968
|
-
}
|
|
972
|
+
// Note: legacy installs may carry a stray `emdash:manifest_cache`
|
|
973
|
+
// row in the options table from versions that persisted a JSON
|
|
974
|
+
// manifest. The runtime no longer reads or writes it. We do not
|
|
975
|
+
// proactively delete it: the row is a few hundred bytes of dead
|
|
976
|
+
// weight and is never on the read path, whereas a one-shot
|
|
977
|
+
// cleanup-flag check costs an extra `options.get()` on every
|
|
978
|
+
// isolate cold boot forever. Cheaper to leave it.
|
|
969
979
|
|
|
970
980
|
// Auto-seed schema if no collections exist and setup hasn't run.
|
|
971
981
|
// This covers first-load on sites that skip the setup wizard.
|
|
@@ -1227,80 +1237,35 @@ export class EmDashRuntime {
|
|
|
1227
1237
|
// =========================================================================
|
|
1228
1238
|
|
|
1229
1239
|
/**
|
|
1230
|
-
*
|
|
1231
|
-
*
|
|
1232
|
-
*
|
|
1240
|
+
* Build the admin manifest from the live database.
|
|
1241
|
+
*
|
|
1242
|
+
* Used by the admin UI (sidebar collections, content editor field
|
|
1243
|
+
* dispatch, manifest endpoint) and by WordPress import — it's never
|
|
1244
|
+
* read on a public request, so this isn't on any anonymous hot path.
|
|
1233
1245
|
*
|
|
1234
|
-
*
|
|
1235
|
-
*
|
|
1246
|
+
* No cross-request cache. The previous worker-isolate cache produced
|
|
1247
|
+
* a class of cross-isolate staleness bugs (#776, #873, #876, #877)
|
|
1248
|
+
* because Cloudflare Workers keeps multiple warm isolates per region
|
|
1249
|
+
* and there's no fan-out primitive to invalidate them in step. The
|
|
1250
|
+
* cache existed to amortize an N+1 schema query pattern; now that
|
|
1251
|
+
* `listCollectionsWithFields()` does the same work in two queries,
|
|
1252
|
+
* the rebuild is fast enough to pay on every admin request.
|
|
1253
|
+
*
|
|
1254
|
+
* Within a single request, `requestCached` deduplicates concurrent
|
|
1255
|
+
* callers (the manifest endpoint and an admin SSR template, say).
|
|
1236
1256
|
*/
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
// DO-preview sessions), bypass the module-scoped manifest cache —
|
|
1240
|
-
// its schema may diverge from the configured DB. Plain D1 Sessions
|
|
1241
|
-
// routing does NOT set `dbIsIsolated`, so the cache still applies.
|
|
1242
|
-
if (getRequestContext()?.dbIsIsolated) {
|
|
1243
|
-
return this._buildManifest();
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
if (this._cachedManifest) return this._cachedManifest;
|
|
1247
|
-
|
|
1248
|
-
// DB-persisted cache (1 query instead of N+1 rebuild on cold start).
|
|
1249
|
-
// Keyed by SHA of commit + config to bust on deploys. DB-driven
|
|
1250
|
-
// changes (collections, fields, plugins, taxonomies) go through
|
|
1251
|
-
// invalidateManifest().
|
|
1252
|
-
try {
|
|
1253
|
-
const options = new OptionsRepository(this.db);
|
|
1254
|
-
const cached = await options.get<{ key: string; manifest: EmDashManifest }>(
|
|
1255
|
-
"emdash:manifest_cache",
|
|
1256
|
-
);
|
|
1257
|
-
if (cached && cached.key === this._manifestCacheKey && cached.manifest) {
|
|
1258
|
-
this._cachedManifest = cached.manifest;
|
|
1259
|
-
return cached.manifest;
|
|
1260
|
-
}
|
|
1261
|
-
} catch {
|
|
1262
|
-
// Options table may not exist yet
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
// Full rebuild, then persist. Track which promise is current so
|
|
1266
|
-
// an invalidation during the build can't be overwritten.
|
|
1267
|
-
if (!this._manifestPromise) {
|
|
1268
|
-
let manifestPromise: Promise<EmDashManifest>;
|
|
1269
|
-
const isCurrentLoad = () => this._manifestPromise === manifestPromise;
|
|
1270
|
-
manifestPromise = this._loadManifest(isCurrentLoad);
|
|
1271
|
-
this._manifestPromise = manifestPromise;
|
|
1272
|
-
}
|
|
1273
|
-
return this._manifestPromise;
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
private async _loadManifest(isCurrentLoad: () => boolean): Promise<EmDashManifest> {
|
|
1277
|
-
try {
|
|
1278
|
-
const manifest = await this._buildManifest();
|
|
1279
|
-
|
|
1280
|
-
if (isCurrentLoad()) {
|
|
1281
|
-
this._cachedManifest = manifest;
|
|
1282
|
-
|
|
1283
|
-
try {
|
|
1284
|
-
const options = new OptionsRepository(this.db);
|
|
1285
|
-
await options.set("emdash:manifest_cache", {
|
|
1286
|
-
key: this._manifestCacheKey,
|
|
1287
|
-
manifest,
|
|
1288
|
-
});
|
|
1289
|
-
} catch {
|
|
1290
|
-
// Non-fatal — will just rebuild next time
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
return manifest;
|
|
1295
|
-
} finally {
|
|
1296
|
-
if (isCurrentLoad()) {
|
|
1297
|
-
this._manifestPromise = null;
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1257
|
+
getManifest(): Promise<EmDashManifest> {
|
|
1258
|
+
return requestCached("emdash:manifest", () => this._buildManifest());
|
|
1300
1259
|
}
|
|
1301
1260
|
|
|
1302
1261
|
/**
|
|
1303
|
-
* Build the manifest from database
|
|
1262
|
+
* Build the manifest from the database.
|
|
1263
|
+
*
|
|
1264
|
+
* Constant query shapes via `listCollectionsWithFields()` — one query
|
|
1265
|
+
* for collections, one batched query for fields (chunked at
|
|
1266
|
+
* `SQL_BATCH_SIZE` collection IDs to stay under D1's bound-parameter
|
|
1267
|
+
* limit). Typical sites stay well under the chunk threshold, so this
|
|
1268
|
+
* is two queries in practice; never N+1.
|
|
1304
1269
|
*/
|
|
1305
1270
|
private async _buildManifest(): Promise<EmDashManifest> {
|
|
1306
1271
|
// Build collections from database.
|
|
@@ -1309,9 +1274,8 @@ export class EmDashRuntime {
|
|
|
1309
1274
|
const manifestCollections: Record<string, ManifestCollection> = {};
|
|
1310
1275
|
try {
|
|
1311
1276
|
const registry = new SchemaRegistry(this.db);
|
|
1312
|
-
const dbCollections = await registry.
|
|
1277
|
+
const dbCollections = await registry.listCollectionsWithFields();
|
|
1313
1278
|
for (const collection of dbCollections) {
|
|
1314
|
-
const collectionWithFields = await registry.getCollectionWithFields(collection.slug);
|
|
1315
1279
|
const fields: Record<
|
|
1316
1280
|
string,
|
|
1317
1281
|
{
|
|
@@ -1326,34 +1290,32 @@ export class EmDashRuntime {
|
|
|
1326
1290
|
}
|
|
1327
1291
|
> = {};
|
|
1328
1292
|
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
(entry as Record<string, unknown>).validation = field.validation;
|
|
1354
|
-
}
|
|
1355
|
-
fields[field.slug] = entry;
|
|
1293
|
+
for (const field of collection.fields) {
|
|
1294
|
+
const entry: (typeof fields)[string] = {
|
|
1295
|
+
kind: FIELD_TYPE_TO_KIND[field.type] ?? "string",
|
|
1296
|
+
label: field.label,
|
|
1297
|
+
required: field.required,
|
|
1298
|
+
};
|
|
1299
|
+
if (field.widget) entry.widget = field.widget;
|
|
1300
|
+
// Plugin field widgets read their per-field config from `field.options`,
|
|
1301
|
+
// which the seed schema types as `Record<string, unknown>`. Pass it
|
|
1302
|
+
// through to the manifest so plugin widgets in the admin SPA receive it.
|
|
1303
|
+
if (field.options) {
|
|
1304
|
+
entry.options = field.options;
|
|
1305
|
+
}
|
|
1306
|
+
// Legacy: select/multiSelect enum options live on `field.validation.options`.
|
|
1307
|
+
// Wins over `field.options` to preserve existing behavior for enum widgets.
|
|
1308
|
+
if (field.validation?.options) {
|
|
1309
|
+
entry.options = field.validation.options.map((v) => ({
|
|
1310
|
+
value: v,
|
|
1311
|
+
label: v.charAt(0).toUpperCase() + v.slice(1),
|
|
1312
|
+
}));
|
|
1313
|
+
}
|
|
1314
|
+
// Include full validation for repeater fields (subFields, minItems, maxItems)
|
|
1315
|
+
if (field.type === "repeater" && field.validation) {
|
|
1316
|
+
(entry as Record<string, unknown>).validation = field.validation;
|
|
1356
1317
|
}
|
|
1318
|
+
fields[field.slug] = entry;
|
|
1357
1319
|
}
|
|
1358
1320
|
|
|
1359
1321
|
manifestCollections[collection.slug] = {
|
|
@@ -1390,6 +1352,7 @@ export class EmDashRuntime {
|
|
|
1390
1352
|
description?: string;
|
|
1391
1353
|
placeholder?: string;
|
|
1392
1354
|
fields?: Element[];
|
|
1355
|
+
category?: string;
|
|
1393
1356
|
}>;
|
|
1394
1357
|
fieldWidgets?: Array<{
|
|
1395
1358
|
name: string;
|
|
@@ -1489,7 +1452,7 @@ export class EmDashRuntime {
|
|
|
1489
1452
|
label: row.label,
|
|
1490
1453
|
labelSingular: row.label_singular ?? undefined,
|
|
1491
1454
|
hierarchical: row.hierarchical === 1,
|
|
1492
|
-
collections:
|
|
1455
|
+
collections: parseStringArray(row.collections).toSorted(),
|
|
1493
1456
|
}));
|
|
1494
1457
|
} catch (error) {
|
|
1495
1458
|
console.debug("EmDash: Could not load taxonomy definitions:", error);
|
|
@@ -1526,27 +1489,6 @@ export class EmDashRuntime {
|
|
|
1526
1489
|
};
|
|
1527
1490
|
}
|
|
1528
1491
|
|
|
1529
|
-
/**
|
|
1530
|
-
* Invalidate cached data derived from the manifest/schema.
|
|
1531
|
-
* Called when collections, fields, plugins, or taxonomy defs change.
|
|
1532
|
-
*/
|
|
1533
|
-
invalidateManifest(): void {
|
|
1534
|
-
this._cachedManifest = null;
|
|
1535
|
-
this._manifestPromise = null;
|
|
1536
|
-
invalidateUrlPatternCache();
|
|
1537
|
-
// Delete DB-persisted cache so the next cold start rebuilds.
|
|
1538
|
-
// Fire-and-forget: in-memory is already cleared for this worker,
|
|
1539
|
-
// DB delete is best-effort for the next cold start.
|
|
1540
|
-
try {
|
|
1541
|
-
const options = new OptionsRepository(this.db);
|
|
1542
|
-
options.delete("emdash:manifest_cache").catch((error) => {
|
|
1543
|
-
console.error("Failed to delete persisted manifest cache", error);
|
|
1544
|
-
});
|
|
1545
|
-
} catch (error) {
|
|
1546
|
-
console.error("Failed to initialize manifest cache invalidation", error);
|
|
1547
|
-
}
|
|
1548
|
-
}
|
|
1549
|
-
|
|
1550
1492
|
/**
|
|
1551
1493
|
* Verify and repair FTS indexes on demand. Runs at most once per worker
|
|
1552
1494
|
* lifetime.
|
|
@@ -1615,11 +1557,75 @@ export class EmDashRuntime {
|
|
|
1615
1557
|
}
|
|
1616
1558
|
|
|
1617
1559
|
async handleContentGet(collection: string, id: string, locale?: string) {
|
|
1618
|
-
|
|
1560
|
+
const result = await handleContentGet(this.db, collection, id, locale);
|
|
1561
|
+
return this.hydrateDraftData(result);
|
|
1619
1562
|
}
|
|
1620
1563
|
|
|
1621
1564
|
async handleContentGetIncludingTrashed(collection: string, id: string, locale?: string) {
|
|
1622
|
-
|
|
1565
|
+
const result = await handleContentGetIncludingTrashed(this.db, collection, id, locale);
|
|
1566
|
+
return this.hydrateDraftData(result);
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
/**
|
|
1570
|
+
* If the response item has a `draftRevisionId`, replace `item.data` with
|
|
1571
|
+
* the draft revision's data and expose the original published values as
|
|
1572
|
+
* `liveData`. This makes the content_get / content_update round-trip
|
|
1573
|
+
* intuitive — read returns the latest content the caller has saved
|
|
1574
|
+
* (their pending draft), with the previously-published values still
|
|
1575
|
+
* accessible for compare-style flows.
|
|
1576
|
+
*
|
|
1577
|
+
* No-op when no draft exists or the response is an error.
|
|
1578
|
+
*/
|
|
1579
|
+
private async hydrateDraftData<T>(result: T): Promise<T> {
|
|
1580
|
+
if (!result || typeof result !== "object") return result;
|
|
1581
|
+
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- shape probed below
|
|
1582
|
+
const r = result as {
|
|
1583
|
+
success?: boolean;
|
|
1584
|
+
data?: { item?: Record<string, unknown> };
|
|
1585
|
+
};
|
|
1586
|
+
if (!r.success || !r.data?.item) return result;
|
|
1587
|
+
const item = r.data.item;
|
|
1588
|
+
const draftRevisionId = typeof item.draftRevisionId === "string" ? item.draftRevisionId : null;
|
|
1589
|
+
if (!draftRevisionId) return result;
|
|
1590
|
+
try {
|
|
1591
|
+
const revision = await new RevisionRepository(this.db).findById(draftRevisionId);
|
|
1592
|
+
if (!revision) return result;
|
|
1593
|
+
const liveData =
|
|
1594
|
+
item.data && typeof item.data === "object"
|
|
1595
|
+
? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- narrowed to object above
|
|
1596
|
+
(item.data as Record<string, unknown>)
|
|
1597
|
+
: {};
|
|
1598
|
+
// Strip leading-underscore keys (`_slug`, `_rev`, etc.) from the
|
|
1599
|
+
// revision data — those are handler-internal markers and don't
|
|
1600
|
+
// belong in the surfaced `data` field. Match syncDataColumns at
|
|
1601
|
+
// content.ts:~1119.
|
|
1602
|
+
const revisionData: Record<string, unknown> = {};
|
|
1603
|
+
for (const [key, value] of Object.entries(revision.data)) {
|
|
1604
|
+
if (!key.startsWith("_")) revisionData[key] = value;
|
|
1605
|
+
}
|
|
1606
|
+
const mergedData = { ...liveData, ...revisionData };
|
|
1607
|
+
// Return a clone rather than mutating in place. The response
|
|
1608
|
+
// object isn't retained by the runtime today, but a future
|
|
1609
|
+
// request-cache layer would observe stale-after-mutation bugs;
|
|
1610
|
+
// cloning closes that footgun.
|
|
1611
|
+
// `r.data` was narrowed to `{ item?: ... }` at the top of this
|
|
1612
|
+
// method; spread its other keys (e.g. `_rev`) alongside the
|
|
1613
|
+
// hydrated item without going back through `unknown`.
|
|
1614
|
+
return {
|
|
1615
|
+
...result,
|
|
1616
|
+
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- shape preserved; result has been narrowed to the {success,data:{item}} envelope
|
|
1617
|
+
data: {
|
|
1618
|
+
...r.data,
|
|
1619
|
+
item: { ...item, data: mergedData, liveData },
|
|
1620
|
+
},
|
|
1621
|
+
} as T;
|
|
1622
|
+
} catch (error) {
|
|
1623
|
+
// Non-fatal — fall back to the unhydrated response. Log so the
|
|
1624
|
+
// failure isn't completely silent (the response will look stale
|
|
1625
|
+
// to the caller but no error is raised).
|
|
1626
|
+
console.error("[emdash] draft hydration failed:", error);
|
|
1627
|
+
return result;
|
|
1628
|
+
}
|
|
1623
1629
|
}
|
|
1624
1630
|
|
|
1625
1631
|
async handleContentCreate(
|
|
@@ -1647,6 +1653,20 @@ export class EmDashRuntime {
|
|
|
1647
1653
|
// Normalize media fields (fill dimensions, storageKey, etc.)
|
|
1648
1654
|
processedData = await this.normalizeMediaFields(collection, processedData);
|
|
1649
1655
|
|
|
1656
|
+
// Validate against the collection schema. Hook output is validated
|
|
1657
|
+
// rather than `body.data` so plugins that mutate field values can't
|
|
1658
|
+
// sneak invalid data past.
|
|
1659
|
+
const { validateContentData } = await import("./api/handlers/validation.js");
|
|
1660
|
+
const validation = await validateContentData(this.db, collection, processedData, {
|
|
1661
|
+
partial: false,
|
|
1662
|
+
});
|
|
1663
|
+
if (!validation.ok) {
|
|
1664
|
+
return {
|
|
1665
|
+
success: false as const,
|
|
1666
|
+
error: validation.error,
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1650
1670
|
// Create the content
|
|
1651
1671
|
const result = await handleContentCreate(this.db, collection, {
|
|
1652
1672
|
...body,
|
|
@@ -1672,6 +1692,14 @@ export class EmDashRuntime {
|
|
|
1672
1692
|
status?: string;
|
|
1673
1693
|
authorId?: string | null;
|
|
1674
1694
|
bylines?: Array<{ bylineId: string; roleLabel?: string | null }>;
|
|
1695
|
+
seo?: {
|
|
1696
|
+
title?: string | null;
|
|
1697
|
+
description?: string | null;
|
|
1698
|
+
image?: string | null;
|
|
1699
|
+
canonical?: string | null;
|
|
1700
|
+
noIndex?: boolean;
|
|
1701
|
+
};
|
|
1702
|
+
publishedAt?: string | null;
|
|
1675
1703
|
/** Skip revision creation (used by autosave) */
|
|
1676
1704
|
skipRevision?: boolean;
|
|
1677
1705
|
_rev?: string;
|
|
@@ -1720,6 +1748,19 @@ export class EmDashRuntime {
|
|
|
1720
1748
|
|
|
1721
1749
|
// Normalize media fields (fill dimensions, storageKey, etc.)
|
|
1722
1750
|
processedData = await this.normalizeMediaFields(collection, processedData);
|
|
1751
|
+
|
|
1752
|
+
// Validate field-level shape BEFORE the draft-revision write so
|
|
1753
|
+
// invalid updates can't silently land in revision history.
|
|
1754
|
+
const { validateContentData } = await import("./api/handlers/validation.js");
|
|
1755
|
+
const validation = await validateContentData(this.db, collection, processedData, {
|
|
1756
|
+
partial: true,
|
|
1757
|
+
});
|
|
1758
|
+
if (!validation.ok) {
|
|
1759
|
+
return {
|
|
1760
|
+
success: false as const,
|
|
1761
|
+
error: validation.error,
|
|
1762
|
+
};
|
|
1763
|
+
}
|
|
1723
1764
|
}
|
|
1724
1765
|
|
|
1725
1766
|
// Draft-aware revision handling (if collection supports revisions)
|
|
@@ -1795,12 +1836,18 @@ export class EmDashRuntime {
|
|
|
1795
1836
|
bylines: bodyWithoutRev.bylines,
|
|
1796
1837
|
});
|
|
1797
1838
|
|
|
1839
|
+
// Hydrate draft data BEFORE firing afterSave hooks so the hook sees
|
|
1840
|
+
// the same effective data the response surfaces — for revision-
|
|
1841
|
+
// supporting collections, that's the just-saved draft, not the live
|
|
1842
|
+
// columns.
|
|
1843
|
+
const hydrated = await this.hydrateDraftData(result);
|
|
1844
|
+
|
|
1798
1845
|
// Run afterSave hooks (fire-and-forget)
|
|
1799
|
-
if (
|
|
1800
|
-
this.runAfterSaveHooks(contentItemToRecord(
|
|
1846
|
+
if (hydrated.success && hydrated.data) {
|
|
1847
|
+
this.runAfterSaveHooks(contentItemToRecord(hydrated.data.item), collection, false);
|
|
1801
1848
|
}
|
|
1802
1849
|
|
|
1803
|
-
return
|
|
1850
|
+
return hydrated;
|
|
1804
1851
|
}
|
|
1805
1852
|
|
|
1806
1853
|
async handleContentDelete(collection: string, id: string) {
|
|
@@ -1879,8 +1926,12 @@ export class EmDashRuntime {
|
|
|
1879
1926
|
// Publishing & Scheduling Handlers
|
|
1880
1927
|
// =========================================================================
|
|
1881
1928
|
|
|
1882
|
-
async handleContentPublish(
|
|
1883
|
-
|
|
1929
|
+
async handleContentPublish(
|
|
1930
|
+
collection: string,
|
|
1931
|
+
id: string,
|
|
1932
|
+
options: { publishedAt?: string } = {},
|
|
1933
|
+
) {
|
|
1934
|
+
const result = await handleContentPublish(this.db, collection, id, options);
|
|
1884
1935
|
|
|
1885
1936
|
// Run afterPublish hooks (fire-and-forget)
|
|
1886
1937
|
if (result.success && result.data) {
|
|
@@ -1947,6 +1998,7 @@ export class EmDashRuntime {
|
|
|
1947
1998
|
contentHash?: string;
|
|
1948
1999
|
blurhash?: string;
|
|
1949
2000
|
dominantColor?: string;
|
|
2001
|
+
authorId?: string;
|
|
1950
2002
|
}) {
|
|
1951
2003
|
// Run beforeUpload hooks
|
|
1952
2004
|
let processedInput = input;
|
|
@@ -2010,7 +2062,74 @@ export class EmDashRuntime {
|
|
|
2010
2062
|
}
|
|
2011
2063
|
|
|
2012
2064
|
async handleRevisionRestore(revisionId: string, callerUserId: string) {
|
|
2013
|
-
|
|
2065
|
+
// Discover the parent entry up front so we can branch on whether
|
|
2066
|
+
// the collection uses draft revisions.
|
|
2067
|
+
const revisionRepo = new RevisionRepository(this.db);
|
|
2068
|
+
const revision = await revisionRepo.findById(revisionId);
|
|
2069
|
+
if (!revision) {
|
|
2070
|
+
return {
|
|
2071
|
+
success: false as const,
|
|
2072
|
+
error: {
|
|
2073
|
+
code: "NOT_FOUND",
|
|
2074
|
+
message: `Revision not found: ${revisionId}`,
|
|
2075
|
+
},
|
|
2076
|
+
};
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
const collectionInfo = await this.schemaRegistry.getCollectionWithFields(revision.collection);
|
|
2080
|
+
const usesDraftRevisions = collectionInfo?.supports?.includes("revisions") ?? false;
|
|
2081
|
+
|
|
2082
|
+
// Non-revision collections: keep the legacy behavior of writing the
|
|
2083
|
+
// revision's data straight onto the live row. This preserves
|
|
2084
|
+
// behavior for collections that opt out of the draft model.
|
|
2085
|
+
if (!usesDraftRevisions) {
|
|
2086
|
+
const result = await handleRevisionRestore(this.db, revisionId, callerUserId);
|
|
2087
|
+
return this.hydrateDraftData(result);
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
// Revision-capable collections: restore is "make this revision the
|
|
2091
|
+
// current draft". The live row's data columns are left untouched
|
|
2092
|
+
// (only `draft_revision_id` and `updated_at` change). The caller
|
|
2093
|
+
// must then `content_publish` to promote the restored draft to
|
|
2094
|
+
// live, matching the documented tool contract.
|
|
2095
|
+
try {
|
|
2096
|
+
const newDraft = await revisionRepo.create({
|
|
2097
|
+
collection: revision.collection,
|
|
2098
|
+
entryId: revision.entryId,
|
|
2099
|
+
data: revision.data,
|
|
2100
|
+
authorId: callerUserId,
|
|
2101
|
+
});
|
|
2102
|
+
|
|
2103
|
+
validateIdentifier(revision.collection, "collection");
|
|
2104
|
+
const tableName = `ec_${revision.collection}`;
|
|
2105
|
+
await sql`
|
|
2106
|
+
UPDATE ${sql.ref(tableName)}
|
|
2107
|
+
SET draft_revision_id = ${newDraft.id},
|
|
2108
|
+
updated_at = ${new Date().toISOString()}
|
|
2109
|
+
WHERE id = ${revision.entryId}
|
|
2110
|
+
`.execute(this.db);
|
|
2111
|
+
|
|
2112
|
+
// Fire-and-forget: prune old revisions to prevent unbounded growth
|
|
2113
|
+
void revisionRepo
|
|
2114
|
+
.pruneOldRevisions(revision.collection, revision.entryId, 50)
|
|
2115
|
+
.catch(() => {});
|
|
2116
|
+
|
|
2117
|
+
// Return the freshly-fetched item with the new draft hydrated
|
|
2118
|
+
// onto `data`. Without this the response would echo the live
|
|
2119
|
+
// columns and the next `content_get` would surface different
|
|
2120
|
+
// values (the bug that motivated this rewrite).
|
|
2121
|
+
const refetched = await handleContentGet(this.db, revision.collection, revision.entryId);
|
|
2122
|
+
return this.hydrateDraftData(refetched);
|
|
2123
|
+
} catch (error) {
|
|
2124
|
+
console.error("[emdash] revision restore failed:", error);
|
|
2125
|
+
return {
|
|
2126
|
+
success: false as const,
|
|
2127
|
+
error: {
|
|
2128
|
+
code: "REVISION_RESTORE_ERROR",
|
|
2129
|
+
message: "Failed to restore revision",
|
|
2130
|
+
},
|
|
2131
|
+
};
|
|
2132
|
+
}
|
|
2014
2133
|
}
|
|
2015
2134
|
|
|
2016
2135
|
// =========================================================================
|
|
@@ -2222,22 +2341,34 @@ export class EmDashRuntime {
|
|
|
2222
2341
|
collection: string,
|
|
2223
2342
|
isNew: boolean,
|
|
2224
2343
|
): void {
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
this.hooks
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
const [id] = pluginKey.split(":");
|
|
2235
|
-
if (!id || !this.isPluginEnabled(id)) continue;
|
|
2344
|
+
after(async () => {
|
|
2345
|
+
// Trusted plugins
|
|
2346
|
+
if (this.hooks.hasHooks("content:afterSave")) {
|
|
2347
|
+
try {
|
|
2348
|
+
await this.hooks.runContentAfterSave(content, collection, isNew);
|
|
2349
|
+
} catch (err) {
|
|
2350
|
+
console.error("EmDash afterSave hook error:", err);
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2236
2353
|
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2354
|
+
// Sandboxed plugins
|
|
2355
|
+
const tasks: Promise<void>[] = [];
|
|
2356
|
+
for (const [pluginKey, plugin] of this.sandboxedPlugins) {
|
|
2357
|
+
const [id] = pluginKey.split(":");
|
|
2358
|
+
if (!id || !this.isPluginEnabled(id)) continue;
|
|
2359
|
+
|
|
2360
|
+
tasks.push(
|
|
2361
|
+
(async () => {
|
|
2362
|
+
try {
|
|
2363
|
+
await plugin.invokeHook("content:afterSave", { content, collection, isNew });
|
|
2364
|
+
} catch (err) {
|
|
2365
|
+
console.error(`EmDash: Sandboxed plugin ${id} afterSave error:`, err);
|
|
2366
|
+
}
|
|
2367
|
+
})(),
|
|
2368
|
+
);
|
|
2369
|
+
}
|
|
2370
|
+
await Promise.allSettled(tasks);
|
|
2371
|
+
});
|
|
2241
2372
|
}
|
|
2242
2373
|
|
|
2243
2374
|
private runAfterDeleteHooks(id: string, collection: string, permanent: boolean): void {
|
|
@@ -2262,24 +2393,34 @@ export class EmDashRuntime {
|
|
|
2262
2393
|
}
|
|
2263
2394
|
|
|
2264
2395
|
private runAfterPublishHooks(content: Record<string, unknown>, collection: string): void {
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
this.hooks
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
const [pluginId] = pluginKey.split(":");
|
|
2275
|
-
if (!pluginId || !this.isPluginEnabled(pluginId)) continue;
|
|
2396
|
+
after(async () => {
|
|
2397
|
+
// Trusted plugins
|
|
2398
|
+
if (this.hooks.hasHooks("content:afterPublish")) {
|
|
2399
|
+
try {
|
|
2400
|
+
await this.hooks.runContentAfterPublish(content, collection);
|
|
2401
|
+
} catch (err) {
|
|
2402
|
+
console.error("EmDash afterPublish hook error:", err);
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2276
2405
|
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2406
|
+
// Sandboxed plugins
|
|
2407
|
+
const tasks: Promise<void>[] = [];
|
|
2408
|
+
for (const [pluginKey, plugin] of this.sandboxedPlugins) {
|
|
2409
|
+
const [pluginId] = pluginKey.split(":");
|
|
2410
|
+
if (!pluginId || !this.isPluginEnabled(pluginId)) continue;
|
|
2411
|
+
|
|
2412
|
+
tasks.push(
|
|
2413
|
+
(async () => {
|
|
2414
|
+
try {
|
|
2415
|
+
await plugin.invokeHook("content:afterPublish", { content, collection });
|
|
2416
|
+
} catch (err) {
|
|
2417
|
+
console.error(`EmDash: Sandboxed plugin ${pluginId} afterPublish error:`, err);
|
|
2418
|
+
}
|
|
2419
|
+
})(),
|
|
2281
2420
|
);
|
|
2282
|
-
|
|
2421
|
+
}
|
|
2422
|
+
await Promise.allSettled(tasks);
|
|
2423
|
+
});
|
|
2283
2424
|
}
|
|
2284
2425
|
|
|
2285
2426
|
private runAfterUnpublishHooks(content: Record<string, unknown>, collection: string): void {
|