emdash 0.0.0-a → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -1
- package/dist/adapters-BLMa4JGD.d.mts +106 -0
- package/dist/adapters-BLMa4JGD.d.mts.map +1 -0
- package/dist/apply-Bjfq_b4-.mjs +1293 -0
- package/dist/apply-Bjfq_b4-.mjs.map +1 -0
- package/dist/astro/index.d.mts +51 -0
- package/dist/astro/index.d.mts.map +1 -0
- package/dist/astro/index.mjs +1333 -0
- package/dist/astro/index.mjs.map +1 -0
- package/dist/astro/middleware/auth.d.mts +31 -0
- package/dist/astro/middleware/auth.d.mts.map +1 -0
- package/dist/astro/middleware/auth.mjs +654 -0
- package/dist/astro/middleware/auth.mjs.map +1 -0
- package/dist/astro/middleware/redirect.d.mts +22 -0
- package/dist/astro/middleware/redirect.d.mts.map +1 -0
- package/dist/astro/middleware/redirect.mjs +63 -0
- package/dist/astro/middleware/redirect.mjs.map +1 -0
- package/dist/astro/middleware/request-context.d.mts +18 -0
- package/dist/astro/middleware/request-context.d.mts.map +1 -0
- package/dist/astro/middleware/request-context.mjs +1310 -0
- package/dist/astro/middleware/request-context.mjs.map +1 -0
- package/dist/astro/middleware/setup.d.mts +20 -0
- package/dist/astro/middleware/setup.d.mts.map +1 -0
- package/dist/astro/middleware/setup.mjs +47 -0
- package/dist/astro/middleware/setup.mjs.map +1 -0
- package/dist/astro/middleware.d.mts +13 -0
- package/dist/astro/middleware.d.mts.map +1 -0
- package/dist/astro/middleware.mjs +1613 -0
- package/dist/astro/middleware.mjs.map +1 -0
- package/dist/astro/types.d.mts +250 -0
- package/dist/astro/types.d.mts.map +1 -0
- package/dist/astro/types.mjs +1 -0
- package/dist/base64-MBPo9ozB.mjs +59 -0
- package/dist/base64-MBPo9ozB.mjs.map +1 -0
- package/dist/byline-CL847F26.mjs +213 -0
- package/dist/byline-CL847F26.mjs.map +1 -0
- package/dist/bylines-C2a-2TGt.mjs +136 -0
- package/dist/bylines-C2a-2TGt.mjs.map +1 -0
- package/dist/chunk-ClPoSABd.mjs +21 -0
- package/dist/cli/index.d.mts +1 -0
- package/dist/cli/index.mjs +3909 -0
- package/dist/cli/index.mjs.map +1 -0
- package/dist/client/cf-access.d.mts +60 -0
- package/dist/client/cf-access.d.mts.map +1 -0
- package/dist/client/cf-access.mjs +179 -0
- package/dist/client/cf-access.mjs.map +1 -0
- package/dist/client/index.d.mts +398 -0
- package/dist/client/index.d.mts.map +1 -0
- package/dist/client/index.mjs +346 -0
- package/dist/client/index.mjs.map +1 -0
- package/dist/config-CKE8p9xM.mjs +55 -0
- package/dist/config-CKE8p9xM.mjs.map +1 -0
- package/dist/connection-B4zVnQIa.mjs +40 -0
- package/dist/connection-B4zVnQIa.mjs.map +1 -0
- package/dist/content-D6C2WsZC.mjs +824 -0
- package/dist/content-D6C2WsZC.mjs.map +1 -0
- package/dist/db/index.d.mts +4 -0
- package/dist/db/index.mjs +62 -0
- package/dist/db/index.mjs.map +1 -0
- package/dist/db/libsql.d.mts +11 -0
- package/dist/db/libsql.d.mts.map +1 -0
- package/dist/db/libsql.mjs +17 -0
- package/dist/db/libsql.mjs.map +1 -0
- package/dist/db/postgres.d.mts +11 -0
- package/dist/db/postgres.d.mts.map +1 -0
- package/dist/db/postgres.mjs +30 -0
- package/dist/db/postgres.mjs.map +1 -0
- package/dist/db/sqlite.d.mts +11 -0
- package/dist/db/sqlite.d.mts.map +1 -0
- package/dist/db/sqlite.mjs +16 -0
- package/dist/db/sqlite.mjs.map +1 -0
- package/dist/default-Cyi4aAxu.mjs +81 -0
- package/dist/default-Cyi4aAxu.mjs.map +1 -0
- package/dist/dialect-helpers-B9uSp2GJ.mjs +90 -0
- package/dist/dialect-helpers-B9uSp2GJ.mjs.map +1 -0
- package/dist/error-Cxz0tQeO.mjs +27 -0
- package/dist/error-Cxz0tQeO.mjs.map +1 -0
- package/dist/index-C1xF3OGh.d.mts +4527 -0
- package/dist/index-C1xF3OGh.d.mts.map +1 -0
- package/dist/index.d.mts +16 -0
- package/dist/index.mjs +30 -0
- package/dist/load-yOOlckBj.mjs +28 -0
- package/dist/load-yOOlckBj.mjs.map +1 -0
- package/dist/loader-fz8Q_3EO.mjs +447 -0
- package/dist/loader-fz8Q_3EO.mjs.map +1 -0
- package/dist/manifest-schema-Dcl0R6nM.mjs +184 -0
- package/dist/manifest-schema-Dcl0R6nM.mjs.map +1 -0
- package/dist/media/index.d.mts +26 -0
- package/dist/media/index.d.mts.map +1 -0
- package/dist/media/index.mjs +55 -0
- package/dist/media/index.mjs.map +1 -0
- package/dist/media/local-runtime.d.mts +39 -0
- package/dist/media/local-runtime.d.mts.map +1 -0
- package/dist/media/local-runtime.mjs +133 -0
- package/dist/media/local-runtime.mjs.map +1 -0
- package/dist/media-DqHVh136.mjs +200 -0
- package/dist/media-DqHVh136.mjs.map +1 -0
- package/dist/mode-C2EzN1uE.mjs +23 -0
- package/dist/mode-C2EzN1uE.mjs.map +1 -0
- package/dist/page/index.d.mts +140 -0
- package/dist/page/index.d.mts.map +1 -0
- package/dist/page/index.mjs +416 -0
- package/dist/page/index.mjs.map +1 -0
- package/dist/placeholder-CmGAmqeO.d.mts +276 -0
- package/dist/placeholder-CmGAmqeO.d.mts.map +1 -0
- package/dist/placeholder-SmpOx-_v.mjs +243 -0
- package/dist/placeholder-SmpOx-_v.mjs.map +1 -0
- package/dist/plugin-utils.d.mts +58 -0
- package/dist/plugin-utils.d.mts.map +1 -0
- package/dist/plugin-utils.mjs +78 -0
- package/dist/plugin-utils.mjs.map +1 -0
- package/dist/plugins/adapt-sandbox-entry.d.mts +22 -0
- package/dist/plugins/adapt-sandbox-entry.d.mts.map +1 -0
- package/dist/plugins/adapt-sandbox-entry.mjs +113 -0
- package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -0
- package/dist/query-CS_iSj34.mjs +460 -0
- package/dist/query-CS_iSj34.mjs.map +1 -0
- package/dist/redirect-DIfIni3r.mjs +329 -0
- package/dist/redirect-DIfIni3r.mjs.map +1 -0
- package/dist/registry-D_w5HW4G.mjs +863 -0
- package/dist/registry-D_w5HW4G.mjs.map +1 -0
- package/dist/request-context.d.mts +49 -0
- package/dist/request-context.d.mts.map +1 -0
- package/dist/request-context.mjs +43 -0
- package/dist/request-context.mjs.map +1 -0
- package/dist/runner-B-u2F2b6.mjs +1412 -0
- package/dist/runner-B-u2F2b6.mjs.map +1 -0
- package/dist/runner-EAtf0ZIe.d.mts +27 -0
- package/dist/runner-EAtf0ZIe.d.mts.map +1 -0
- package/dist/runtime.d.mts +26 -0
- package/dist/runtime.d.mts.map +1 -0
- package/dist/runtime.mjs +42 -0
- package/dist/runtime.mjs.map +1 -0
- package/dist/search-DG603UrT.mjs +9211 -0
- package/dist/search-DG603UrT.mjs.map +1 -0
- package/dist/seed/index.d.mts +3 -0
- package/dist/seed/index.mjs +15 -0
- package/dist/seo/index.d.mts +70 -0
- package/dist/seo/index.d.mts.map +1 -0
- package/dist/seo/index.mjs +70 -0
- package/dist/seo/index.mjs.map +1 -0
- package/dist/storage/local.d.mts +39 -0
- package/dist/storage/local.d.mts.map +1 -0
- package/dist/storage/local.mjs +166 -0
- package/dist/storage/local.mjs.map +1 -0
- package/dist/storage/s3.d.mts +32 -0
- package/dist/storage/s3.d.mts.map +1 -0
- package/dist/storage/s3.mjs +175 -0
- package/dist/storage/s3.mjs.map +1 -0
- package/dist/tokens-DpgrkrXK.mjs +171 -0
- package/dist/tokens-DpgrkrXK.mjs.map +1 -0
- package/dist/transport-BFGblqwG.d.mts +42 -0
- package/dist/transport-BFGblqwG.d.mts.map +1 -0
- package/dist/transport-yxiQsi8I.mjs +418 -0
- package/dist/transport-yxiQsi8I.mjs.map +1 -0
- package/dist/types-BRuPJGdV.d.mts +102 -0
- package/dist/types-BRuPJGdV.d.mts.map +1 -0
- package/dist/types-C4-fAxN3.d.mts +182 -0
- package/dist/types-C4-fAxN3.d.mts.map +1 -0
- package/dist/types-CMMN0pNg.mjs +31 -0
- package/dist/types-CMMN0pNg.mjs.map +1 -0
- package/dist/types-CUBbjgmP.mjs +16 -0
- package/dist/types-CUBbjgmP.mjs.map +1 -0
- package/dist/types-DRjfYOEv.d.mts +426 -0
- package/dist/types-DRjfYOEv.d.mts.map +1 -0
- package/dist/types-DY5zk5HN.mjs +73 -0
- package/dist/types-DY5zk5HN.mjs.map +1 -0
- package/dist/types-DaNLHo_T.d.mts +184 -0
- package/dist/types-DaNLHo_T.d.mts.map +1 -0
- package/dist/types-DvhsUmSJ.d.mts +1111 -0
- package/dist/types-DvhsUmSJ.d.mts.map +1 -0
- package/dist/validate-CpBtVMsD.d.mts +378 -0
- package/dist/validate-CpBtVMsD.d.mts.map +1 -0
- package/dist/validate-CqRJb_xU.mjs +97 -0
- package/dist/validate-CqRJb_xU.mjs.map +1 -0
- package/dist/validate-O7PWmlnq.mjs +328 -0
- package/dist/validate-O7PWmlnq.mjs.map +1 -0
- package/locals.d.ts +46 -0
- package/package.json +233 -19
- package/src/api/authorize.ts +63 -0
- package/src/api/csrf.ts +48 -0
- package/src/api/error.ts +99 -0
- package/src/api/errors.ts +445 -0
- package/src/api/escape.ts +9 -0
- package/src/api/handlers/api-tokens.ts +240 -0
- package/src/api/handlers/comments.ts +314 -0
- package/src/api/handlers/content.ts +1315 -0
- package/src/api/handlers/dashboard.ts +205 -0
- package/src/api/handlers/device-flow.ts +687 -0
- package/src/api/handlers/index.ts +163 -0
- package/src/api/handlers/manifest.ts +158 -0
- package/src/api/handlers/marketplace.ts +930 -0
- package/src/api/handlers/media.ts +207 -0
- package/src/api/handlers/menus.ts +493 -0
- package/src/api/handlers/oauth-authorization.ts +429 -0
- package/src/api/handlers/oauth-clients.ts +353 -0
- package/src/api/handlers/oauth-user-lookup.ts +39 -0
- package/src/api/handlers/plugins.ts +254 -0
- package/src/api/handlers/redirects.ts +360 -0
- package/src/api/handlers/revision.ts +145 -0
- package/src/api/handlers/schema.ts +534 -0
- package/src/api/handlers/sections.ts +289 -0
- package/src/api/handlers/seo.ts +115 -0
- package/src/api/handlers/settings.ts +49 -0
- package/src/api/handlers/snapshot.ts +350 -0
- package/src/api/handlers/taxonomies.ts +523 -0
- package/src/api/index.ts +6 -0
- package/src/api/openapi/document.ts +2368 -0
- package/src/api/openapi/index.ts +1 -0
- package/src/api/parse.ts +139 -0
- package/src/api/redirect.ts +14 -0
- package/src/api/rev.ts +67 -0
- package/src/api/schemas/auth.ts +112 -0
- package/src/api/schemas/bylines.ts +85 -0
- package/src/api/schemas/comments.ts +117 -0
- package/src/api/schemas/common.ts +89 -0
- package/src/api/schemas/content.ts +191 -0
- package/src/api/schemas/import.ts +52 -0
- package/src/api/schemas/index.ts +17 -0
- package/src/api/schemas/media.ts +116 -0
- package/src/api/schemas/menus.ts +111 -0
- package/src/api/schemas/redirects.ts +155 -0
- package/src/api/schemas/schema.ts +203 -0
- package/src/api/schemas/search.ts +63 -0
- package/src/api/schemas/sections.ts +67 -0
- package/src/api/schemas/settings.ts +63 -0
- package/src/api/schemas/setup.ts +37 -0
- package/src/api/schemas/taxonomies.ts +113 -0
- package/src/api/schemas/users.ts +96 -0
- package/src/api/schemas/widgets.ts +80 -0
- package/src/api/site-url.ts +25 -0
- package/src/api/types.ts +82 -0
- package/src/astro/index.ts +27 -0
- package/src/astro/integration/index.ts +303 -0
- package/src/astro/integration/routes.ts +834 -0
- package/src/astro/integration/runtime.ts +338 -0
- package/src/astro/integration/virtual-modules.ts +469 -0
- package/src/astro/integration/vite-config.ts +328 -0
- package/src/astro/middleware/auth.ts +743 -0
- package/src/astro/middleware/redirect.ts +89 -0
- package/src/astro/middleware/request-context.ts +129 -0
- package/src/astro/middleware/setup.ts +89 -0
- package/src/astro/middleware.ts +398 -0
- package/src/astro/routes/PluginRegistry.tsx +15 -0
- package/src/astro/routes/admin.astro +81 -0
- package/src/astro/routes/api/admin/allowed-domains/[domain].ts +112 -0
- package/src/astro/routes/api/admin/allowed-domains/index.ts +108 -0
- package/src/astro/routes/api/admin/api-tokens/[id].ts +40 -0
- package/src/astro/routes/api/admin/api-tokens/index.ts +68 -0
- package/src/astro/routes/api/admin/bylines/[id]/index.ts +87 -0
- package/src/astro/routes/api/admin/bylines/index.ts +72 -0
- package/src/astro/routes/api/admin/comments/[id]/status.ts +120 -0
- package/src/astro/routes/api/admin/comments/[id].ts +64 -0
- package/src/astro/routes/api/admin/comments/bulk.ts +42 -0
- package/src/astro/routes/api/admin/comments/counts.ts +30 -0
- package/src/astro/routes/api/admin/comments/index.ts +46 -0
- package/src/astro/routes/api/admin/hooks/exclusive/[hookName].ts +91 -0
- package/src/astro/routes/api/admin/hooks/exclusive/index.ts +51 -0
- package/src/astro/routes/api/admin/oauth-clients/[id].ts +110 -0
- package/src/astro/routes/api/admin/oauth-clients/index.ts +71 -0
- package/src/astro/routes/api/admin/plugins/[id]/disable.ts +39 -0
- package/src/astro/routes/api/admin/plugins/[id]/enable.ts +39 -0
- package/src/astro/routes/api/admin/plugins/[id]/index.ts +38 -0
- package/src/astro/routes/api/admin/plugins/[id]/uninstall.ts +48 -0
- package/src/astro/routes/api/admin/plugins/[id]/update.ts +59 -0
- package/src/astro/routes/api/admin/plugins/index.ts +32 -0
- package/src/astro/routes/api/admin/plugins/marketplace/[id]/icon.ts +61 -0
- package/src/astro/routes/api/admin/plugins/marketplace/[id]/index.ts +33 -0
- package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +62 -0
- package/src/astro/routes/api/admin/plugins/marketplace/index.ts +38 -0
- package/src/astro/routes/api/admin/plugins/updates.ts +28 -0
- package/src/astro/routes/api/admin/themes/marketplace/[id]/index.ts +33 -0
- package/src/astro/routes/api/admin/themes/marketplace/[id]/thumbnail.ts +61 -0
- package/src/astro/routes/api/admin/themes/marketplace/index.ts +45 -0
- package/src/astro/routes/api/admin/users/[id]/disable.ts +69 -0
- package/src/astro/routes/api/admin/users/[id]/enable.ts +48 -0
- package/src/astro/routes/api/admin/users/[id]/index.ts +146 -0
- package/src/astro/routes/api/admin/users/[id]/send-recovery.ts +72 -0
- package/src/astro/routes/api/admin/users/index.ts +66 -0
- package/src/astro/routes/api/auth/dev-bypass.ts +139 -0
- package/src/astro/routes/api/auth/invite/accept.ts +52 -0
- package/src/astro/routes/api/auth/invite/complete.ts +84 -0
- package/src/astro/routes/api/auth/invite/index.ts +99 -0
- package/src/astro/routes/api/auth/logout.ts +40 -0
- package/src/astro/routes/api/auth/magic-link/send.ts +89 -0
- package/src/astro/routes/api/auth/magic-link/verify.ts +71 -0
- package/src/astro/routes/api/auth/me.ts +60 -0
- package/src/astro/routes/api/auth/oauth/[provider]/callback.ts +219 -0
- package/src/astro/routes/api/auth/oauth/[provider].ts +119 -0
- package/src/astro/routes/api/auth/passkey/[id].ts +124 -0
- package/src/astro/routes/api/auth/passkey/index.ts +54 -0
- package/src/astro/routes/api/auth/passkey/options.ts +82 -0
- package/src/astro/routes/api/auth/passkey/register/options.ts +86 -0
- package/src/astro/routes/api/auth/passkey/register/verify.ts +117 -0
- package/src/astro/routes/api/auth/passkey/verify.ts +66 -0
- package/src/astro/routes/api/auth/signup/complete.ts +85 -0
- package/src/astro/routes/api/auth/signup/request.ts +77 -0
- package/src/astro/routes/api/auth/signup/verify.ts +53 -0
- package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +312 -0
- package/src/astro/routes/api/content/[collection]/[id]/compare.ts +28 -0
- package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +54 -0
- package/src/astro/routes/api/content/[collection]/[id]/duplicate.ts +61 -0
- package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +33 -0
- package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +107 -0
- package/src/astro/routes/api/content/[collection]/[id]/publish.ts +56 -0
- package/src/astro/routes/api/content/[collection]/[id]/restore.ts +54 -0
- package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +31 -0
- package/src/astro/routes/api/content/[collection]/[id]/schedule.ts +105 -0
- package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +140 -0
- package/src/astro/routes/api/content/[collection]/[id]/translations.ts +30 -0
- package/src/astro/routes/api/content/[collection]/[id]/unpublish.ts +56 -0
- package/src/astro/routes/api/content/[collection]/[id].ts +137 -0
- package/src/astro/routes/api/content/[collection]/index.ts +59 -0
- package/src/astro/routes/api/content/[collection]/trash.ts +33 -0
- package/src/astro/routes/api/dashboard.ts +32 -0
- package/src/astro/routes/api/dev/emails.ts +36 -0
- package/src/astro/routes/api/import/probe.ts +47 -0
- package/src/astro/routes/api/import/wordpress/analyze.ts +510 -0
- package/src/astro/routes/api/import/wordpress/execute.ts +283 -0
- package/src/astro/routes/api/import/wordpress/media.ts +338 -0
- package/src/astro/routes/api/import/wordpress/prepare.ts +181 -0
- package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +393 -0
- package/src/astro/routes/api/import/wordpress-plugin/analyze.ts +111 -0
- package/src/astro/routes/api/import/wordpress-plugin/callback.ts +58 -0
- package/src/astro/routes/api/import/wordpress-plugin/execute.ts +347 -0
- package/src/astro/routes/api/manifest.ts +62 -0
- package/src/astro/routes/api/mcp.ts +124 -0
- package/src/astro/routes/api/media/[id]/confirm.ts +93 -0
- package/src/astro/routes/api/media/[id].ts +145 -0
- package/src/astro/routes/api/media/file/[key].ts +79 -0
- package/src/astro/routes/api/media/providers/[providerId]/[itemId].ts +86 -0
- package/src/astro/routes/api/media/providers/[providerId]/index.ts +111 -0
- package/src/astro/routes/api/media/providers/index.ts +30 -0
- package/src/astro/routes/api/media/upload-url.ts +137 -0
- package/src/astro/routes/api/media.ts +190 -0
- package/src/astro/routes/api/menus/[name]/items.ts +87 -0
- package/src/astro/routes/api/menus/[name]/reorder.ts +33 -0
- package/src/astro/routes/api/menus/[name].ts +65 -0
- package/src/astro/routes/api/menus/index.ts +47 -0
- package/src/astro/routes/api/oauth/authorize.ts +412 -0
- package/src/astro/routes/api/oauth/device/authorize.ts +45 -0
- package/src/astro/routes/api/oauth/device/code.ts +51 -0
- package/src/astro/routes/api/oauth/device/token.ts +69 -0
- package/src/astro/routes/api/oauth/token/refresh.ts +38 -0
- package/src/astro/routes/api/oauth/token/revoke.ts +38 -0
- package/src/astro/routes/api/oauth/token.ts +184 -0
- package/src/astro/routes/api/openapi.json.ts +32 -0
- package/src/astro/routes/api/plugins/[pluginId]/[...path].ts +92 -0
- package/src/astro/routes/api/redirects/404s/index.ts +72 -0
- package/src/astro/routes/api/redirects/404s/summary.ts +33 -0
- package/src/astro/routes/api/redirects/[id].ts +84 -0
- package/src/astro/routes/api/redirects/index.ts +52 -0
- package/src/astro/routes/api/revisions/[revisionId]/index.ts +29 -0
- package/src/astro/routes/api/revisions/[revisionId]/restore.ts +62 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +76 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +52 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +32 -0
- package/src/astro/routes/api/schema/collections/[slug]/index.ts +80 -0
- package/src/astro/routes/api/schema/collections/index.ts +47 -0
- package/src/astro/routes/api/schema/index.ts +109 -0
- package/src/astro/routes/api/schema/orphans/[slug].ts +36 -0
- package/src/astro/routes/api/schema/orphans/index.ts +26 -0
- package/src/astro/routes/api/search/enable.ts +64 -0
- package/src/astro/routes/api/search/index.ts +55 -0
- package/src/astro/routes/api/search/rebuild.ts +72 -0
- package/src/astro/routes/api/search/stats.ts +35 -0
- package/src/astro/routes/api/search/suggest.ts +53 -0
- package/src/astro/routes/api/sections/[slug].ts +84 -0
- package/src/astro/routes/api/sections/index.ts +52 -0
- package/src/astro/routes/api/settings/email.ts +150 -0
- package/src/astro/routes/api/settings.ts +67 -0
- package/src/astro/routes/api/setup/admin-verify.ts +100 -0
- package/src/astro/routes/api/setup/admin.ts +94 -0
- package/src/astro/routes/api/setup/dev-bypass.ts +199 -0
- package/src/astro/routes/api/setup/dev-reset.ts +40 -0
- package/src/astro/routes/api/setup/index.ts +126 -0
- package/src/astro/routes/api/setup/status.ts +122 -0
- package/src/astro/routes/api/snapshot.ts +75 -0
- package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +95 -0
- package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +69 -0
- package/src/astro/routes/api/taxonomies/index.ts +59 -0
- package/src/astro/routes/api/themes/preview.ts +77 -0
- package/src/astro/routes/api/typegen.ts +114 -0
- package/src/astro/routes/api/well-known/auth.ts +68 -0
- package/src/astro/routes/api/well-known/oauth-authorization-server.ts +44 -0
- package/src/astro/routes/api/well-known/oauth-protected-resource.ts +37 -0
- package/src/astro/routes/api/widget-areas/[name]/reorder.ts +72 -0
- package/src/astro/routes/api/widget-areas/[name]/widgets/[id].ts +127 -0
- package/src/astro/routes/api/widget-areas/[name]/widgets.ts +80 -0
- package/src/astro/routes/api/widget-areas/[name].ts +87 -0
- package/src/astro/routes/api/widget-areas/index.ts +99 -0
- package/src/astro/routes/api/widget-components.ts +22 -0
- package/src/astro/routes/robots.txt.ts +77 -0
- package/src/astro/routes/sitemap.xml.ts +97 -0
- package/src/astro/storage/adapters.ts +74 -0
- package/src/astro/storage/index.ts +19 -0
- package/src/astro/storage/types.ts +60 -0
- package/src/astro/types.ts +346 -0
- package/src/auth/api-tokens.ts +25 -0
- package/src/auth/challenge-store.ts +80 -0
- package/src/auth/mode.ts +96 -0
- package/src/auth/oauth-state-store.ts +96 -0
- package/src/auth/passkey-config.ts +27 -0
- package/src/auth/rate-limit.ts +158 -0
- package/src/auth/scopes.ts +33 -0
- package/src/auth/types.ts +104 -0
- package/src/aws-sdk.d.ts +100 -0
- package/src/bylines/index.ts +237 -0
- package/src/cleanup.ts +153 -0
- package/src/cli/client-factory.ts +100 -0
- package/src/cli/commands/auth.ts +46 -0
- package/src/cli/commands/bundle-utils.ts +247 -0
- package/src/cli/commands/bundle.ts +609 -0
- package/src/cli/commands/content.ts +442 -0
- package/src/cli/commands/dev.ts +191 -0
- package/src/cli/commands/doctor.ts +211 -0
- package/src/cli/commands/export-seed.ts +630 -0
- package/src/cli/commands/import/wordpress.ts +1056 -0
- package/src/cli/commands/init.ts +192 -0
- package/src/cli/commands/login.ts +547 -0
- package/src/cli/commands/media.ts +165 -0
- package/src/cli/commands/menu.ts +67 -0
- package/src/cli/commands/plugin-init.ts +291 -0
- package/src/cli/commands/plugin-validate.ts +31 -0
- package/src/cli/commands/plugin.ts +33 -0
- package/src/cli/commands/publish.ts +699 -0
- package/src/cli/commands/schema.ts +233 -0
- package/src/cli/commands/search-cmd.ts +54 -0
- package/src/cli/commands/seed.ts +288 -0
- package/src/cli/commands/taxonomy.ts +128 -0
- package/src/cli/commands/types.ts +68 -0
- package/src/cli/credentials.ts +236 -0
- package/src/cli/index.ts +70 -0
- package/src/cli/output.ts +75 -0
- package/src/cli/wxr/parser.ts +969 -0
- package/src/client/cf-access.ts +193 -0
- package/src/client/index.ts +854 -0
- package/src/client/portable-text.ts +413 -0
- package/src/client/transport.ts +200 -0
- package/src/comments/moderator.ts +46 -0
- package/src/comments/notifications.ts +144 -0
- package/src/comments/query.ts +105 -0
- package/src/comments/service.ts +213 -0
- package/src/components/Break.astro +45 -0
- package/src/components/Button.astro +71 -0
- package/src/components/Buttons.astro +49 -0
- package/src/components/Code.astro +59 -0
- package/src/components/Columns.astro +59 -0
- package/src/components/CommentForm.astro +315 -0
- package/src/components/Comments.astro +232 -0
- package/src/components/Cover.astro +128 -0
- package/src/components/EmDashBodyEnd.astro +32 -0
- package/src/components/EmDashBodyStart.astro +32 -0
- package/src/components/EmDashHead.astro +53 -0
- package/src/components/EmDashImage.astro +178 -0
- package/src/components/EmDashMedia.astro +167 -0
- package/src/components/Embed.astro +128 -0
- package/src/components/File.astro +122 -0
- package/src/components/Gallery.astro +93 -0
- package/src/components/HtmlBlock.astro +33 -0
- package/src/components/Image.astro +178 -0
- package/src/components/InlineEditor.astro +27 -0
- package/src/components/InlinePortableTextEditor.tsx +1905 -0
- package/src/components/LiveSearch.astro +614 -0
- package/src/components/PortableText.astro +51 -0
- package/src/components/Pullquote.astro +51 -0
- package/src/components/Table.astro +108 -0
- package/src/components/WidgetArea.astro +22 -0
- package/src/components/WidgetRenderer.astro +72 -0
- package/src/components/index.ts +116 -0
- package/src/components/marks/Link.astro +31 -0
- package/src/components/marks/StrikeThrough.astro +7 -0
- package/src/components/marks/Subscript.astro +7 -0
- package/src/components/marks/Superscript.astro +7 -0
- package/src/components/marks/Underline.astro +7 -0
- package/src/components/widgets/Archives.astro +65 -0
- package/src/components/widgets/Categories.astro +35 -0
- package/src/components/widgets/RecentPosts.astro +51 -0
- package/src/components/widgets/Search.astro +18 -0
- package/src/components/widgets/Tags.astro +38 -0
- package/src/content/converters/index.ts +9 -0
- package/src/content/converters/portable-text-to-prosemirror.ts +385 -0
- package/src/content/converters/prosemirror-to-portable-text.ts +413 -0
- package/src/content/converters/types.ts +120 -0
- package/src/content/index.ts +5 -0
- package/src/database/connection.ts +67 -0
- package/src/database/dialect-helpers.ts +138 -0
- package/src/database/index.ts +5 -0
- package/src/database/migrations/001_initial.ts +136 -0
- package/src/database/migrations/002_media_status.ts +26 -0
- package/src/database/migrations/003_schema_registry.ts +79 -0
- package/src/database/migrations/004_plugins.ts +62 -0
- package/src/database/migrations/005_menus.ts +67 -0
- package/src/database/migrations/006_taxonomy_defs.ts +51 -0
- package/src/database/migrations/007_widgets.ts +42 -0
- package/src/database/migrations/008_auth.ts +194 -0
- package/src/database/migrations/009_user_disabled.ts +27 -0
- package/src/database/migrations/011_sections.ts +65 -0
- package/src/database/migrations/012_search.ts +25 -0
- package/src/database/migrations/013_scheduled_publishing.ts +51 -0
- package/src/database/migrations/014_draft_revisions.ts +72 -0
- package/src/database/migrations/015_indexes.ts +82 -0
- package/src/database/migrations/016_api_tokens.ts +89 -0
- package/src/database/migrations/017_authorization_codes.ts +45 -0
- package/src/database/migrations/018_seo.ts +56 -0
- package/src/database/migrations/019_i18n.ts +618 -0
- package/src/database/migrations/020_collection_url_pattern.ts +23 -0
- package/src/database/migrations/021_remove_section_categories.ts +43 -0
- package/src/database/migrations/022_marketplace_plugin_state.ts +46 -0
- package/src/database/migrations/023_plugin_metadata.ts +33 -0
- package/src/database/migrations/024_media_placeholders.ts +32 -0
- package/src/database/migrations/025_oauth_clients.ts +28 -0
- package/src/database/migrations/026_cron_tasks.ts +49 -0
- package/src/database/migrations/027_comments.ts +87 -0
- package/src/database/migrations/028_drop_author_url.ts +9 -0
- package/src/database/migrations/029_redirects.ts +67 -0
- package/src/database/migrations/030_widen_scheduled_index.ts +48 -0
- package/src/database/migrations/031_bylines.ts +90 -0
- package/src/database/migrations/032_rate_limits.ts +42 -0
- package/src/database/migrations/runner.ts +170 -0
- package/src/database/repositories/audit.ts +294 -0
- package/src/database/repositories/byline.ts +387 -0
- package/src/database/repositories/comment.ts +458 -0
- package/src/database/repositories/content.ts +1144 -0
- package/src/database/repositories/index.ts +30 -0
- package/src/database/repositories/media.ts +347 -0
- package/src/database/repositories/options.ts +150 -0
- package/src/database/repositories/plugin-storage.ts +373 -0
- package/src/database/repositories/redirect.ts +480 -0
- package/src/database/repositories/revision.ts +200 -0
- package/src/database/repositories/seo.ts +176 -0
- package/src/database/repositories/taxonomy.ts +294 -0
- package/src/database/repositories/types.ts +132 -0
- package/src/database/repositories/user.ts +258 -0
- package/src/database/transaction.ts +54 -0
- package/src/database/types.ts +501 -0
- package/src/database/validate.ts +138 -0
- package/src/db/adapters.ts +125 -0
- package/src/db/index.ts +37 -0
- package/src/db/libsql.ts +23 -0
- package/src/db/postgres.ts +30 -0
- package/src/db/sqlite.ts +27 -0
- package/src/emdash-runtime.ts +2096 -0
- package/src/fields/boolean.ts +34 -0
- package/src/fields/datetime.ts +44 -0
- package/src/fields/file.ts +41 -0
- package/src/fields/image.ts +34 -0
- package/src/fields/index.ts +42 -0
- package/src/fields/integer.ts +50 -0
- package/src/fields/json.ts +37 -0
- package/src/fields/multiselect.ts +48 -0
- package/src/fields/number.ts +52 -0
- package/src/fields/portable-text.ts +33 -0
- package/src/fields/reference.ts +29 -0
- package/src/fields/richtext.ts +31 -0
- package/src/fields/select.ts +46 -0
- package/src/fields/slug.ts +38 -0
- package/src/fields/text.ts +55 -0
- package/src/fields/textarea.ts +52 -0
- package/src/fields/types.ts +64 -0
- package/src/i18n/config.ts +68 -0
- package/src/import/index.ts +90 -0
- package/src/import/menus.ts +436 -0
- package/src/import/registry.ts +111 -0
- package/src/import/sections.ts +103 -0
- package/src/import/settings.ts +281 -0
- package/src/import/sources/wordpress-plugin.ts +641 -0
- package/src/import/sources/wordpress-rest.ts +191 -0
- package/src/import/sources/wxr.ts +330 -0
- package/src/import/ssrf.ts +260 -0
- package/src/import/types.ts +418 -0
- package/src/import/utils.ts +412 -0
- package/src/index.ts +481 -0
- package/src/loader.ts +770 -0
- package/src/mcp/server.ts +1463 -0
- package/src/media/index.ts +32 -0
- package/src/media/local-runtime.ts +213 -0
- package/src/media/local.ts +46 -0
- package/src/media/normalize.ts +190 -0
- package/src/media/placeholder.ts +150 -0
- package/src/media/provider-loader.ts +78 -0
- package/src/media/types.ts +279 -0
- package/src/menus/index.ts +324 -0
- package/src/menus/types.ts +112 -0
- package/src/page/context.ts +93 -0
- package/src/page/fragments.ts +89 -0
- package/src/page/index.ts +58 -0
- package/src/page/jsonld.ts +94 -0
- package/src/page/metadata.ts +185 -0
- package/src/page/seo-contributions.ts +136 -0
- package/src/plugin-utils.ts +80 -0
- package/src/plugins/adapt-sandbox-entry.ts +207 -0
- package/src/plugins/context.ts +833 -0
- package/src/plugins/cron.ts +361 -0
- package/src/plugins/define-plugin.ts +259 -0
- package/src/plugins/email-console.ts +73 -0
- package/src/plugins/email.ts +209 -0
- package/src/plugins/hooks.ts +1273 -0
- package/src/plugins/index.ts +193 -0
- package/src/plugins/manager.ts +595 -0
- package/src/plugins/manifest-schema.ts +230 -0
- package/src/plugins/marketplace.ts +460 -0
- package/src/plugins/request-meta.ts +139 -0
- package/src/plugins/routes.ts +302 -0
- package/src/plugins/sandbox/index.ts +18 -0
- package/src/plugins/sandbox/noop.ts +76 -0
- package/src/plugins/sandbox/types.ts +173 -0
- package/src/plugins/scheduler/node.ts +122 -0
- package/src/plugins/scheduler/piggyback.ts +71 -0
- package/src/plugins/scheduler/types.ts +27 -0
- package/src/plugins/state.ts +208 -0
- package/src/plugins/storage-indexes.ts +326 -0
- package/src/plugins/storage-query.ts +240 -0
- package/src/plugins/types.ts +1284 -0
- package/src/preview/helpers.ts +27 -0
- package/src/preview/index.ts +40 -0
- package/src/preview/tokens.ts +279 -0
- package/src/preview/urls.ts +118 -0
- package/src/query.ts +674 -0
- package/src/redirects/patterns.ts +224 -0
- package/src/request-context.ts +67 -0
- package/src/runtime.ts +21 -0
- package/src/schema/index.ts +29 -0
- package/src/schema/query.ts +44 -0
- package/src/schema/registry.ts +965 -0
- package/src/schema/types.ts +276 -0
- package/src/schema/zod-generator.ts +413 -0
- package/src/search/fts-manager.ts +452 -0
- package/src/search/index.ts +26 -0
- package/src/search/query.ts +396 -0
- package/src/search/text-extraction.ts +162 -0
- package/src/search/types.ts +114 -0
- package/src/sections/index.ts +226 -0
- package/src/sections/types.ts +86 -0
- package/src/seed/apply.ts +1141 -0
- package/src/seed/default.ts +86 -0
- package/src/seed/index.ts +28 -0
- package/src/seed/load.ts +35 -0
- package/src/seed/types.ts +341 -0
- package/src/seed/validate.ts +642 -0
- package/src/seo/index.ts +179 -0
- package/src/settings/index.ts +203 -0
- package/src/settings/types.ts +58 -0
- package/src/storage/index.ts +28 -0
- package/src/storage/local.ts +253 -0
- package/src/storage/s3.ts +271 -0
- package/src/storage/types.ts +204 -0
- package/src/taxonomies/index.ts +309 -0
- package/src/taxonomies/types.ts +61 -0
- package/src/ui.ts +75 -0
- package/src/utils/base64.ts +73 -0
- package/src/utils/hash.ts +36 -0
- package/src/utils/sanitize.ts +20 -0
- package/src/utils/slugify.ts +29 -0
- package/src/utils/url.ts +48 -0
- package/src/virtual-modules.d.ts +111 -0
- package/src/visual-editing/editable.ts +108 -0
- package/src/visual-editing/toolbar.ts +1229 -0
- package/src/widgets/components.ts +105 -0
- package/src/widgets/index.ts +131 -0
- package/src/widgets/types.ts +81 -0
|
@@ -0,0 +1,1613 @@
|
|
|
1
|
+
import "../connection-B4zVnQIa.mjs";
|
|
2
|
+
import { a as isSqlite } from "../dialect-helpers-B9uSp2GJ.mjs";
|
|
3
|
+
import { r as runMigrations } from "../runner-B-u2F2b6.mjs";
|
|
4
|
+
import { $ as definePlugin, At as handleContentGetIncludingTrashed, Bt as handleContentUpdate, Ct as handleContentCountScheduled, Dt as handleContentDiscardDraft, Et as handleContentDelete, Ft as handleContentRestore, G as devConsoleEmailDeliver, It as handleContentSchedule, J as createHookPipeline, K as EmailPipeline, Lt as handleContentTranslations, Mt as handleContentListTrashed, Nt as handleContentPermanentDelete, Ot as handleContentDuplicate, Pt as handleContentPublish, Q as sanitizeHeadersForSandbox, Rt as handleContentUnpublish, St as handleContentCompare, Tt as handleContentCreate, U as PluginRouteRegistry, Vt as validateRev, W as DEV_CONSOLE_EMAIL_PLUGIN_ID, X as CronExecutor, Y as resolveExclusiveHooks, Z as extractRequestMeta, _t as handleRevisionList, dt as handleMediaCreate, ft as handleMediaDelete, gt as handleRevisionGet, ht as handleMediaUpdate, jt as handleContentList, kt as handleContentGet, lt as PluginStateRepository, mt as handleMediaList, ot as loadBundleFromR2, pt as handleMediaGet, vt as handleRevisionRestore, wt as handleContentCountTrashed, xt as hashString, zt as handleContentUnschedule } from "../search-DG603UrT.mjs";
|
|
5
|
+
import { r as RevisionRepository } from "../content-D6C2WsZC.mjs";
|
|
6
|
+
import "../base64-MBPo9ozB.mjs";
|
|
7
|
+
import "../types-CMMN0pNg.mjs";
|
|
8
|
+
import { t as MediaRepository } from "../media-DqHVh136.mjs";
|
|
9
|
+
import { u as OptionsRepository } from "../apply-Bjfq_b4-.mjs";
|
|
10
|
+
import { i as FTSManager, n as SchemaRegistry } from "../registry-D_w5HW4G.mjs";
|
|
11
|
+
import "../redirect-DIfIni3r.mjs";
|
|
12
|
+
import "../byline-CL847F26.mjs";
|
|
13
|
+
import { n as normalizeMediaValue } from "../placeholder-SmpOx-_v.mjs";
|
|
14
|
+
import { a as setI18nConfig } from "../config-CKE8p9xM.mjs";
|
|
15
|
+
import { getRequestContext, runWithContext } from "../request-context.mjs";
|
|
16
|
+
import { n as getDb } from "../loader-fz8Q_3EO.mjs";
|
|
17
|
+
import { r as normalizeManifestRoute } from "../manifest-schema-Dcl0R6nM.mjs";
|
|
18
|
+
import "../query-CS_iSj34.mjs";
|
|
19
|
+
import "../tokens-DpgrkrXK.mjs";
|
|
20
|
+
import "../bylines-C2a-2TGt.mjs";
|
|
21
|
+
import "../load-yOOlckBj.mjs";
|
|
22
|
+
import "../index.mjs";
|
|
23
|
+
import { t as getAuthMode } from "../mode-C2EzN1uE.mjs";
|
|
24
|
+
import { Kysely, sql } from "kysely";
|
|
25
|
+
import { defineMiddleware } from "astro:middleware";
|
|
26
|
+
import virtualConfig from "virtual:emdash/config";
|
|
27
|
+
import { createDialect, createSessionDialect, getBookmarkCookieName, getD1Binding, getDefaultConstraint, isSessionEnabled } from "virtual:emdash/dialect";
|
|
28
|
+
import { mediaProviders } from "virtual:emdash/media-providers";
|
|
29
|
+
import { plugins } from "virtual:emdash/plugins";
|
|
30
|
+
import { createSandboxRunner, sandboxEnabled } from "virtual:emdash/sandbox-runner";
|
|
31
|
+
import { sandboxedPlugins } from "virtual:emdash/sandboxed-plugins";
|
|
32
|
+
import { createStorage } from "virtual:emdash/storage";
|
|
33
|
+
import { createKyselyAdapter } from "@emdash-cms/auth/adapters/kysely";
|
|
34
|
+
|
|
35
|
+
//#region src/auth/challenge-store.ts
|
|
36
|
+
/**
|
|
37
|
+
* Clean up expired challenges.
|
|
38
|
+
* Should be called periodically (e.g., on startup, or via cron).
|
|
39
|
+
*/
|
|
40
|
+
async function cleanupExpiredChallenges(db) {
|
|
41
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
42
|
+
const result = await db.deleteFrom("auth_challenges").where("expires_at", "<", now).executeTakeFirst();
|
|
43
|
+
return Number(result.numDeletedRows ?? 0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/cleanup.ts
|
|
48
|
+
/**
|
|
49
|
+
* System cleanup
|
|
50
|
+
*
|
|
51
|
+
* Runs periodic maintenance tasks that prevent unbounded accumulation of
|
|
52
|
+
* expired or stale data. Called from cron scheduler ticks and (for latency-
|
|
53
|
+
* sensitive subsystems) inline during relevant requests.
|
|
54
|
+
*
|
|
55
|
+
* Each subsystem cleanup is independent and non-fatal -- if one fails, the
|
|
56
|
+
* rest still run. Failures are logged but never surface to callers.
|
|
57
|
+
*/
|
|
58
|
+
/** Max revisions to keep per entry during periodic pruning */
|
|
59
|
+
const REVISION_KEEP_COUNT = 50;
|
|
60
|
+
/** Only prune entries that exceed this threshold */
|
|
61
|
+
const REVISION_PRUNE_THRESHOLD = REVISION_KEEP_COUNT;
|
|
62
|
+
/**
|
|
63
|
+
* Run all system cleanup tasks.
|
|
64
|
+
*
|
|
65
|
+
* Safe to call frequently -- each task is a single DELETE with a WHERE clause,
|
|
66
|
+
* so repeated calls with nothing to clean are cheap (no-op queries).
|
|
67
|
+
*
|
|
68
|
+
* @param db - The database instance
|
|
69
|
+
* @param storage - Optional storage backend for deleting orphaned files.
|
|
70
|
+
* When omitted, pending upload DB rows are still deleted but the
|
|
71
|
+
* corresponding files in object storage are not removed.
|
|
72
|
+
*/
|
|
73
|
+
async function runSystemCleanup(db, storage) {
|
|
74
|
+
const result = {
|
|
75
|
+
challenges: -1,
|
|
76
|
+
expiredTokens: -1,
|
|
77
|
+
pendingUploads: -1,
|
|
78
|
+
pendingUploadFiles: -1,
|
|
79
|
+
revisionsPruned: -1
|
|
80
|
+
};
|
|
81
|
+
try {
|
|
82
|
+
result.challenges = await cleanupExpiredChallenges(db);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error("[cleanup] Failed to clean expired challenges:", error);
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
await createKyselyAdapter(db).deleteExpiredTokens();
|
|
88
|
+
result.expiredTokens = 0;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error("[cleanup] Failed to clean expired tokens:", error);
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const orphanedKeys = await new MediaRepository(db).cleanupPendingUploads();
|
|
94
|
+
result.pendingUploads = orphanedKeys.length;
|
|
95
|
+
if (storage && orphanedKeys.length > 0) {
|
|
96
|
+
let filesDeleted = 0;
|
|
97
|
+
for (const key of orphanedKeys) try {
|
|
98
|
+
await storage.delete(key);
|
|
99
|
+
filesDeleted++;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.error(`[cleanup] Failed to delete storage file ${key}:`, error);
|
|
102
|
+
}
|
|
103
|
+
result.pendingUploadFiles = filesDeleted;
|
|
104
|
+
} else result.pendingUploadFiles = 0;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error("[cleanup] Failed to clean pending uploads:", error);
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
result.revisionsPruned = await pruneExcessiveRevisions(db);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error("[cleanup] Failed to prune revisions:", error);
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Find entries with more than REVISION_PRUNE_THRESHOLD revisions and prune
|
|
117
|
+
* them down to REVISION_KEEP_COUNT.
|
|
118
|
+
*/
|
|
119
|
+
async function pruneExcessiveRevisions(db) {
|
|
120
|
+
const entries = await sql`
|
|
121
|
+
SELECT collection, entry_id, COUNT(*) as cnt
|
|
122
|
+
FROM revisions
|
|
123
|
+
GROUP BY collection, entry_id
|
|
124
|
+
HAVING cnt > ${REVISION_PRUNE_THRESHOLD}
|
|
125
|
+
`.execute(db);
|
|
126
|
+
if (entries.rows.length === 0) return 0;
|
|
127
|
+
const revisionRepo = new RevisionRepository(db);
|
|
128
|
+
let totalPruned = 0;
|
|
129
|
+
for (const row of entries.rows) try {
|
|
130
|
+
const pruned = await revisionRepo.pruneOldRevisions(row.collection, row.entry_id, REVISION_KEEP_COUNT);
|
|
131
|
+
totalPruned += pruned;
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error(`[cleanup] Failed to prune revisions for ${row.collection}/${row.entry_id}:`, error);
|
|
134
|
+
}
|
|
135
|
+
return totalPruned;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
//#endregion
|
|
139
|
+
//#region src/comments/moderator.ts
|
|
140
|
+
/** Plugin ID for the built-in default comment moderator */
|
|
141
|
+
const DEFAULT_COMMENT_MODERATOR_PLUGIN_ID = "emdash-default-comment-moderator";
|
|
142
|
+
/**
|
|
143
|
+
* The comment:moderate handler for the built-in default moderator.
|
|
144
|
+
*/
|
|
145
|
+
async function defaultCommentModerate(event, _ctx) {
|
|
146
|
+
const { comment, collectionSettings, priorApprovedCount } = event;
|
|
147
|
+
if (collectionSettings.commentsAutoApproveUsers && comment.authorUserId) return {
|
|
148
|
+
status: "approved",
|
|
149
|
+
reason: "Authenticated CMS user"
|
|
150
|
+
};
|
|
151
|
+
if (collectionSettings.commentsModeration === "none") return {
|
|
152
|
+
status: "approved",
|
|
153
|
+
reason: "Moderation disabled"
|
|
154
|
+
};
|
|
155
|
+
if (collectionSettings.commentsModeration === "first_time" && priorApprovedCount > 0) return {
|
|
156
|
+
status: "approved",
|
|
157
|
+
reason: "Returning commenter"
|
|
158
|
+
};
|
|
159
|
+
return {
|
|
160
|
+
status: "pending",
|
|
161
|
+
reason: "Held for review"
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
//#endregion
|
|
166
|
+
//#region src/plugins/scheduler/node.ts
|
|
167
|
+
/** Minimum polling interval (ms) — prevents tight loops if next_run_at is in the past */
|
|
168
|
+
const MIN_INTERVAL_MS = 1e3;
|
|
169
|
+
/** Maximum polling interval (ms) — wake up periodically to check for stale locks */
|
|
170
|
+
const MAX_INTERVAL_MS = 300 * 1e3;
|
|
171
|
+
var NodeCronScheduler = class {
|
|
172
|
+
timer = null;
|
|
173
|
+
running = false;
|
|
174
|
+
systemCleanup = null;
|
|
175
|
+
constructor(executor) {
|
|
176
|
+
this.executor = executor;
|
|
177
|
+
}
|
|
178
|
+
setSystemCleanup(fn) {
|
|
179
|
+
this.systemCleanup = fn;
|
|
180
|
+
}
|
|
181
|
+
start() {
|
|
182
|
+
this.running = true;
|
|
183
|
+
this.arm();
|
|
184
|
+
}
|
|
185
|
+
stop() {
|
|
186
|
+
this.running = false;
|
|
187
|
+
if (this.timer) {
|
|
188
|
+
clearTimeout(this.timer);
|
|
189
|
+
this.timer = null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
reschedule() {
|
|
193
|
+
if (!this.running) return;
|
|
194
|
+
if (this.timer) {
|
|
195
|
+
clearTimeout(this.timer);
|
|
196
|
+
this.timer = null;
|
|
197
|
+
}
|
|
198
|
+
this.arm();
|
|
199
|
+
}
|
|
200
|
+
arm() {
|
|
201
|
+
if (!this.running) return;
|
|
202
|
+
this.executor.getNextDueTime().then((nextDue) => {
|
|
203
|
+
if (!this.running) return void 0;
|
|
204
|
+
let delayMs;
|
|
205
|
+
if (nextDue) {
|
|
206
|
+
const dueAt = new Date(nextDue).getTime();
|
|
207
|
+
delayMs = Math.max(dueAt - Date.now(), MIN_INTERVAL_MS);
|
|
208
|
+
delayMs = Math.min(delayMs, MAX_INTERVAL_MS);
|
|
209
|
+
} else delayMs = MAX_INTERVAL_MS;
|
|
210
|
+
this.timer = setTimeout(() => {
|
|
211
|
+
if (!this.running) return;
|
|
212
|
+
this.executeTick();
|
|
213
|
+
}, delayMs);
|
|
214
|
+
if (this.timer && typeof this.timer === "object" && "unref" in this.timer) this.timer.unref();
|
|
215
|
+
}).catch((error) => {
|
|
216
|
+
console.error("[cron:node] Failed to get next due time:", error);
|
|
217
|
+
if (this.running) {
|
|
218
|
+
this.timer = setTimeout(() => this.arm(), MAX_INTERVAL_MS);
|
|
219
|
+
if (this.timer && typeof this.timer === "object" && "unref" in this.timer) this.timer.unref();
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
executeTick() {
|
|
224
|
+
if (!this.running) return;
|
|
225
|
+
const tasks = [this.executor.tick(), this.executor.recoverStaleLocks()];
|
|
226
|
+
if (this.systemCleanup) tasks.push(this.systemCleanup());
|
|
227
|
+
Promise.allSettled(tasks).then((results) => {
|
|
228
|
+
for (const r of results) if (r.status === "rejected") console.error("[cron:node] Tick task failed:", r.reason);
|
|
229
|
+
}).finally(() => {
|
|
230
|
+
if (this.running) this.arm();
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
//#endregion
|
|
236
|
+
//#region src/plugins/scheduler/piggyback.ts
|
|
237
|
+
/** Minimum interval between tick attempts (ms) */
|
|
238
|
+
const DEBOUNCE_MS = 60 * 1e3;
|
|
239
|
+
var PiggybackScheduler = class {
|
|
240
|
+
lastTickAt = 0;
|
|
241
|
+
running = false;
|
|
242
|
+
systemCleanup = null;
|
|
243
|
+
constructor(executor) {
|
|
244
|
+
this.executor = executor;
|
|
245
|
+
}
|
|
246
|
+
setSystemCleanup(fn) {
|
|
247
|
+
this.systemCleanup = fn;
|
|
248
|
+
}
|
|
249
|
+
start() {
|
|
250
|
+
this.running = true;
|
|
251
|
+
}
|
|
252
|
+
stop() {
|
|
253
|
+
this.running = false;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* No-op for piggyback — tick happens on next request.
|
|
257
|
+
*/
|
|
258
|
+
reschedule() {}
|
|
259
|
+
/**
|
|
260
|
+
* Call this from middleware on each request.
|
|
261
|
+
* Debounced: only actually ticks if enough time has passed.
|
|
262
|
+
*/
|
|
263
|
+
onRequest() {
|
|
264
|
+
if (!this.running) return;
|
|
265
|
+
const now = Date.now();
|
|
266
|
+
if (now - this.lastTickAt < DEBOUNCE_MS) return;
|
|
267
|
+
this.lastTickAt = now;
|
|
268
|
+
const tasks = [this.executor.tick(), this.executor.recoverStaleLocks()];
|
|
269
|
+
if (this.systemCleanup) tasks.push(this.systemCleanup());
|
|
270
|
+
Promise.allSettled(tasks).then((results) => {
|
|
271
|
+
for (const r of results) if (r.status === "rejected") console.error("[cron:piggyback] Tick task failed:", r.reason);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
//#endregion
|
|
277
|
+
//#region src/emdash-runtime.ts
|
|
278
|
+
const LEADING_SLASH_PATTERN = /^\//;
|
|
279
|
+
const VALID_METADATA_KINDS = new Set([
|
|
280
|
+
"meta",
|
|
281
|
+
"property",
|
|
282
|
+
"link",
|
|
283
|
+
"jsonld"
|
|
284
|
+
]);
|
|
285
|
+
/** Security-critical allowlist for link rel values from sandboxed plugins */
|
|
286
|
+
const VALID_LINK_REL = new Set([
|
|
287
|
+
"canonical",
|
|
288
|
+
"alternate",
|
|
289
|
+
"author",
|
|
290
|
+
"license",
|
|
291
|
+
"site.standard.document"
|
|
292
|
+
]);
|
|
293
|
+
/**
|
|
294
|
+
* Runtime validation for sandboxed plugin metadata contributions.
|
|
295
|
+
* Sandboxed plugins return `unknown` across the RPC boundary — we must
|
|
296
|
+
* verify the shape before passing to the metadata collector.
|
|
297
|
+
*/
|
|
298
|
+
function isValidMetadataContribution(c) {
|
|
299
|
+
if (!c || typeof c !== "object" || !("kind" in c)) return false;
|
|
300
|
+
const obj = c;
|
|
301
|
+
if (typeof obj.kind !== "string" || !VALID_METADATA_KINDS.has(obj.kind)) return false;
|
|
302
|
+
switch (obj.kind) {
|
|
303
|
+
case "meta": return typeof obj.name === "string" && typeof obj.content === "string";
|
|
304
|
+
case "property": return typeof obj.property === "string" && typeof obj.content === "string";
|
|
305
|
+
case "link": return typeof obj.href === "string" && typeof obj.rel === "string" && VALID_LINK_REL.has(obj.rel);
|
|
306
|
+
case "jsonld": return obj.graph != null && typeof obj.graph === "object";
|
|
307
|
+
default: return false;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Map schema field types to editor field kinds
|
|
312
|
+
*/
|
|
313
|
+
const FIELD_TYPE_TO_KIND = {
|
|
314
|
+
string: "string",
|
|
315
|
+
slug: "string",
|
|
316
|
+
text: "richText",
|
|
317
|
+
number: "number",
|
|
318
|
+
integer: "number",
|
|
319
|
+
boolean: "boolean",
|
|
320
|
+
datetime: "datetime",
|
|
321
|
+
select: "select",
|
|
322
|
+
multiSelect: "multiSelect",
|
|
323
|
+
portableText: "portableText",
|
|
324
|
+
image: "image",
|
|
325
|
+
file: "file",
|
|
326
|
+
reference: "reference",
|
|
327
|
+
json: "json"
|
|
328
|
+
};
|
|
329
|
+
/**
|
|
330
|
+
* Convert a ContentItem to Record<string, unknown> for hook consumption.
|
|
331
|
+
* Hooks receive the full item as a flat record.
|
|
332
|
+
*/
|
|
333
|
+
function contentItemToRecord(item) {
|
|
334
|
+
return { ...item };
|
|
335
|
+
}
|
|
336
|
+
const dbCache = /* @__PURE__ */ new Map();
|
|
337
|
+
let dbInitPromise = null;
|
|
338
|
+
const storageCache = /* @__PURE__ */ new Map();
|
|
339
|
+
const sandboxedPluginCache = /* @__PURE__ */ new Map();
|
|
340
|
+
const marketplacePluginKeys = /* @__PURE__ */ new Set();
|
|
341
|
+
/** Manifest metadata for marketplace plugins: pluginId -> manifest admin config */
|
|
342
|
+
const marketplaceManifestCache = /* @__PURE__ */ new Map();
|
|
343
|
+
/** Route metadata for sandboxed plugins: pluginId -> routeName -> RouteMeta */
|
|
344
|
+
const sandboxedRouteMetaCache = /* @__PURE__ */ new Map();
|
|
345
|
+
let sandboxRunner = null;
|
|
346
|
+
/**
|
|
347
|
+
* EmDashRuntime - singleton per worker
|
|
348
|
+
*/
|
|
349
|
+
var EmDashRuntime = class EmDashRuntime {
|
|
350
|
+
/**
|
|
351
|
+
* The singleton database instance (worker-lifetime cached).
|
|
352
|
+
* Use the `db` getter instead — it checks the request context first
|
|
353
|
+
* for per-request overrides (D1 read replica sessions, DO multi-site).
|
|
354
|
+
*/
|
|
355
|
+
_db;
|
|
356
|
+
storage;
|
|
357
|
+
configuredPlugins;
|
|
358
|
+
sandboxedPlugins;
|
|
359
|
+
sandboxedPluginEntries;
|
|
360
|
+
schemaRegistry;
|
|
361
|
+
_hooks;
|
|
362
|
+
config;
|
|
363
|
+
mediaProviders;
|
|
364
|
+
mediaProviderEntries;
|
|
365
|
+
cronExecutor;
|
|
366
|
+
email;
|
|
367
|
+
cronScheduler;
|
|
368
|
+
enabledPlugins;
|
|
369
|
+
pluginStates;
|
|
370
|
+
/** Current hook pipeline. Use the `hooks` getter for external access. */
|
|
371
|
+
get hooks() {
|
|
372
|
+
return this._hooks;
|
|
373
|
+
}
|
|
374
|
+
/** All plugins eligible for the hook pipeline (includes built-in plugins).
|
|
375
|
+
* Stored so we can rebuild the pipeline when plugins are enabled/disabled. */
|
|
376
|
+
allPipelinePlugins;
|
|
377
|
+
/** Factory options for the hook pipeline context factory */
|
|
378
|
+
pipelineFactoryOptions;
|
|
379
|
+
/** Dependencies needed for exclusive hook resolution */
|
|
380
|
+
runtimeDeps;
|
|
381
|
+
/** Mutable ref for the cron invokeCronHook closure to read the current pipeline */
|
|
382
|
+
pipelineRef;
|
|
383
|
+
/**
|
|
384
|
+
* Get the database instance for the current request.
|
|
385
|
+
*
|
|
386
|
+
* Checks the ALS-based request context first — middleware sets a
|
|
387
|
+
* per-request Kysely instance there for D1 read replica sessions
|
|
388
|
+
* or DO preview databases. Falls back to the singleton instance.
|
|
389
|
+
*/
|
|
390
|
+
get db() {
|
|
391
|
+
const ctx = getRequestContext();
|
|
392
|
+
if (ctx?.db) return ctx.db;
|
|
393
|
+
return this._db;
|
|
394
|
+
}
|
|
395
|
+
constructor(db, storage, configuredPlugins, sandboxedPlugins, sandboxedPluginEntries, hooks, enabledPlugins, pluginStates, config, mediaProviders, mediaProviderEntries, cronExecutor, cronScheduler, emailPipeline, allPipelinePlugins, pipelineFactoryOptions, runtimeDeps, pipelineRef) {
|
|
396
|
+
this._db = db;
|
|
397
|
+
this.storage = storage;
|
|
398
|
+
this.configuredPlugins = configuredPlugins;
|
|
399
|
+
this.sandboxedPlugins = sandboxedPlugins;
|
|
400
|
+
this.sandboxedPluginEntries = sandboxedPluginEntries;
|
|
401
|
+
this.schemaRegistry = new SchemaRegistry(db);
|
|
402
|
+
this._hooks = hooks;
|
|
403
|
+
this.enabledPlugins = enabledPlugins;
|
|
404
|
+
this.pluginStates = pluginStates;
|
|
405
|
+
this.config = config;
|
|
406
|
+
this.mediaProviders = mediaProviders;
|
|
407
|
+
this.mediaProviderEntries = mediaProviderEntries;
|
|
408
|
+
this.cronExecutor = cronExecutor;
|
|
409
|
+
this.cronScheduler = cronScheduler;
|
|
410
|
+
this.email = emailPipeline;
|
|
411
|
+
this.allPipelinePlugins = allPipelinePlugins;
|
|
412
|
+
this.pipelineFactoryOptions = pipelineFactoryOptions;
|
|
413
|
+
this.runtimeDeps = runtimeDeps;
|
|
414
|
+
this.pipelineRef = pipelineRef;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Get the sandbox runner instance (for marketplace install/update)
|
|
418
|
+
*/
|
|
419
|
+
getSandboxRunner() {
|
|
420
|
+
return sandboxRunner;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Tick the cron system from request context (piggyback mode).
|
|
424
|
+
* Call this from middleware on each request to ensure cron tasks
|
|
425
|
+
* execute even when no dedicated scheduler is available.
|
|
426
|
+
*/
|
|
427
|
+
tickCron() {
|
|
428
|
+
if (this.cronScheduler instanceof PiggybackScheduler) this.cronScheduler.onRequest();
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Stop the cron scheduler gracefully.
|
|
432
|
+
* Call during worker shutdown or hot-reload.
|
|
433
|
+
*/
|
|
434
|
+
async stopCron() {
|
|
435
|
+
if (this.cronScheduler) await this.cronScheduler.stop();
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Update in-memory plugin status and rebuild the hook pipeline.
|
|
439
|
+
*
|
|
440
|
+
* Rebuilding the pipeline ensures disabled plugins' hooks stop firing
|
|
441
|
+
* and re-enabled plugins' hooks start firing again without a restart.
|
|
442
|
+
* Exclusive hook selections are re-resolved after each rebuild.
|
|
443
|
+
*/
|
|
444
|
+
async setPluginStatus(pluginId, status) {
|
|
445
|
+
this.pluginStates.set(pluginId, status);
|
|
446
|
+
if (status === "active") this.enabledPlugins.add(pluginId);
|
|
447
|
+
else this.enabledPlugins.delete(pluginId);
|
|
448
|
+
await this.rebuildHookPipeline();
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Rebuild the hook pipeline from the current set of enabled plugins.
|
|
452
|
+
*
|
|
453
|
+
* Filters `allPipelinePlugins` to only those in `enabledPlugins`,
|
|
454
|
+
* creates a fresh HookPipeline, re-resolves exclusive hook selections,
|
|
455
|
+
* and re-wires the context factory so existing references (cron
|
|
456
|
+
* callbacks, email pipeline) use the new pipeline.
|
|
457
|
+
*/
|
|
458
|
+
async rebuildHookPipeline() {
|
|
459
|
+
const newPipeline = createHookPipeline(this.allPipelinePlugins.filter((p) => this.enabledPlugins.has(p.id)), this.pipelineFactoryOptions);
|
|
460
|
+
await EmDashRuntime.resolveExclusiveHooks(newPipeline, this.db, this.runtimeDeps);
|
|
461
|
+
if (this.email) newPipeline.setContextFactory({
|
|
462
|
+
db: this.db,
|
|
463
|
+
emailPipeline: this.email
|
|
464
|
+
});
|
|
465
|
+
if (this.cronScheduler) {
|
|
466
|
+
const scheduler = this.cronScheduler;
|
|
467
|
+
newPipeline.setContextFactory({ cronReschedule: () => scheduler.reschedule() });
|
|
468
|
+
}
|
|
469
|
+
if (this.email) this.email.setPipeline(newPipeline);
|
|
470
|
+
this.pipelineRef.current = newPipeline;
|
|
471
|
+
this._hooks = newPipeline;
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Synchronize marketplace plugin runtime state with DB + storage.
|
|
475
|
+
*
|
|
476
|
+
* Ensures install/update/uninstall changes take effect immediately in the
|
|
477
|
+
* current worker: loads newly active plugins and removes uninstalled ones.
|
|
478
|
+
*/
|
|
479
|
+
async syncMarketplacePlugins() {
|
|
480
|
+
if (!this.config.marketplace || !this.storage) return;
|
|
481
|
+
if (!sandboxRunner || !sandboxRunner.isAvailable()) return;
|
|
482
|
+
try {
|
|
483
|
+
const marketplaceStates = await new PluginStateRepository(this.db).getMarketplacePlugins();
|
|
484
|
+
const desired = /* @__PURE__ */ new Map();
|
|
485
|
+
for (const state of marketplaceStates) {
|
|
486
|
+
this.pluginStates.set(state.pluginId, state.status);
|
|
487
|
+
if (state.status === "active") this.enabledPlugins.add(state.pluginId);
|
|
488
|
+
else this.enabledPlugins.delete(state.pluginId);
|
|
489
|
+
if (state.status !== "active") continue;
|
|
490
|
+
desired.set(state.pluginId, state.marketplaceVersion ?? state.version);
|
|
491
|
+
}
|
|
492
|
+
const keysToRemove = [];
|
|
493
|
+
for (const key of marketplacePluginKeys) {
|
|
494
|
+
const [pluginId] = key.split(":");
|
|
495
|
+
if (!pluginId) continue;
|
|
496
|
+
const desiredVersion = desired.get(pluginId);
|
|
497
|
+
if (desiredVersion && key === `${pluginId}:${desiredVersion}`) continue;
|
|
498
|
+
keysToRemove.push(key);
|
|
499
|
+
}
|
|
500
|
+
for (const key of keysToRemove) {
|
|
501
|
+
const [pluginId] = key.split(":");
|
|
502
|
+
if (!pluginId) continue;
|
|
503
|
+
if (!desired.get(pluginId)) {
|
|
504
|
+
this.pluginStates.delete(pluginId);
|
|
505
|
+
this.enabledPlugins.delete(pluginId);
|
|
506
|
+
}
|
|
507
|
+
const existing = sandboxedPluginCache.get(key);
|
|
508
|
+
if (existing) try {
|
|
509
|
+
await existing.terminate();
|
|
510
|
+
} catch (error) {
|
|
511
|
+
console.warn(`EmDash: Failed to terminate sandboxed plugin ${key}:`, error);
|
|
512
|
+
}
|
|
513
|
+
sandboxedPluginCache.delete(key);
|
|
514
|
+
this.sandboxedPlugins.delete(key);
|
|
515
|
+
marketplacePluginKeys.delete(key);
|
|
516
|
+
if (pluginId) {
|
|
517
|
+
sandboxedRouteMetaCache.delete(pluginId);
|
|
518
|
+
marketplaceManifestCache.delete(pluginId);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
for (const [pluginId, version] of desired) {
|
|
522
|
+
const key = `${pluginId}:${version}`;
|
|
523
|
+
if (sandboxedPluginCache.has(key)) {
|
|
524
|
+
marketplacePluginKeys.add(key);
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
const bundle = await loadBundleFromR2(this.storage, pluginId, version);
|
|
528
|
+
if (!bundle) {
|
|
529
|
+
console.warn(`EmDash: Marketplace plugin ${pluginId}@${version} not found in R2`);
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
const loaded = await sandboxRunner.load(bundle.manifest, bundle.backendCode);
|
|
533
|
+
sandboxedPluginCache.set(key, loaded);
|
|
534
|
+
this.sandboxedPlugins.set(key, loaded);
|
|
535
|
+
marketplacePluginKeys.add(key);
|
|
536
|
+
marketplaceManifestCache.set(pluginId, {
|
|
537
|
+
id: bundle.manifest.id,
|
|
538
|
+
version: bundle.manifest.version,
|
|
539
|
+
admin: bundle.manifest.admin
|
|
540
|
+
});
|
|
541
|
+
if (bundle.manifest.routes.length > 0) {
|
|
542
|
+
const routeMetaMap = /* @__PURE__ */ new Map();
|
|
543
|
+
for (const entry of bundle.manifest.routes) {
|
|
544
|
+
const normalized = normalizeManifestRoute(entry);
|
|
545
|
+
routeMetaMap.set(normalized.name, { public: normalized.public === true });
|
|
546
|
+
}
|
|
547
|
+
sandboxedRouteMetaCache.set(pluginId, routeMetaMap);
|
|
548
|
+
} else sandboxedRouteMetaCache.delete(pluginId);
|
|
549
|
+
}
|
|
550
|
+
} catch (error) {
|
|
551
|
+
console.error("EmDash: Failed to sync marketplace plugins:", error);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Create and initialize the runtime
|
|
556
|
+
*/
|
|
557
|
+
static async create(deps) {
|
|
558
|
+
const db = await EmDashRuntime.getDatabase(deps);
|
|
559
|
+
if (isSqlite(db)) try {
|
|
560
|
+
const repaired = await new FTSManager(db).verifyAndRepairAll();
|
|
561
|
+
if (repaired > 0) console.log(`Repaired ${repaired} corrupted FTS index(es) at startup`);
|
|
562
|
+
} catch {}
|
|
563
|
+
const storage = EmDashRuntime.getStorage(deps);
|
|
564
|
+
let pluginStates = /* @__PURE__ */ new Map();
|
|
565
|
+
try {
|
|
566
|
+
const states = await db.selectFrom("_plugin_state").select(["plugin_id", "status"]).execute();
|
|
567
|
+
pluginStates = new Map(states.map((s) => [s.plugin_id, s.status]));
|
|
568
|
+
} catch {}
|
|
569
|
+
const enabledPlugins = /* @__PURE__ */ new Set();
|
|
570
|
+
for (const plugin of deps.plugins) {
|
|
571
|
+
const status = pluginStates.get(plugin.id);
|
|
572
|
+
if (status === void 0 || status === "active") enabledPlugins.add(plugin.id);
|
|
573
|
+
}
|
|
574
|
+
let siteInfo;
|
|
575
|
+
try {
|
|
576
|
+
const optionsRepo = new OptionsRepository(db);
|
|
577
|
+
const siteName = await optionsRepo.get("emdash:site_title");
|
|
578
|
+
const siteUrl = await optionsRepo.get("emdash:site_url");
|
|
579
|
+
const locale = await optionsRepo.get("emdash:locale");
|
|
580
|
+
siteInfo = {
|
|
581
|
+
siteName: siteName ?? void 0,
|
|
582
|
+
siteUrl: siteUrl ?? void 0,
|
|
583
|
+
locale: locale ?? void 0
|
|
584
|
+
};
|
|
585
|
+
} catch {}
|
|
586
|
+
const allPipelinePlugins = [...deps.plugins];
|
|
587
|
+
if (import.meta.env.DEV) try {
|
|
588
|
+
const devConsolePlugin = definePlugin({
|
|
589
|
+
id: DEV_CONSOLE_EMAIL_PLUGIN_ID,
|
|
590
|
+
version: "0.0.0",
|
|
591
|
+
capabilities: ["email:provide"],
|
|
592
|
+
hooks: { "email:deliver": {
|
|
593
|
+
exclusive: true,
|
|
594
|
+
handler: devConsoleEmailDeliver
|
|
595
|
+
} }
|
|
596
|
+
});
|
|
597
|
+
allPipelinePlugins.push(devConsolePlugin);
|
|
598
|
+
enabledPlugins.add(devConsolePlugin.id);
|
|
599
|
+
} catch (error) {
|
|
600
|
+
console.warn("[email] Failed to register dev console email provider:", error);
|
|
601
|
+
}
|
|
602
|
+
try {
|
|
603
|
+
const defaultModeratorPlugin = definePlugin({
|
|
604
|
+
id: DEFAULT_COMMENT_MODERATOR_PLUGIN_ID,
|
|
605
|
+
version: "0.0.0",
|
|
606
|
+
capabilities: ["read:users"],
|
|
607
|
+
hooks: { "comment:moderate": {
|
|
608
|
+
exclusive: true,
|
|
609
|
+
handler: defaultCommentModerate
|
|
610
|
+
} }
|
|
611
|
+
});
|
|
612
|
+
allPipelinePlugins.push(defaultModeratorPlugin);
|
|
613
|
+
enabledPlugins.add(defaultModeratorPlugin.id);
|
|
614
|
+
} catch (error) {
|
|
615
|
+
console.warn("[comments] Failed to register default moderator:", error);
|
|
616
|
+
}
|
|
617
|
+
const enabledPluginList = allPipelinePlugins.filter((p) => enabledPlugins.has(p.id));
|
|
618
|
+
const pipelineFactoryOptions = {
|
|
619
|
+
db,
|
|
620
|
+
storage: storage ?? void 0,
|
|
621
|
+
siteInfo
|
|
622
|
+
};
|
|
623
|
+
const pipeline = createHookPipeline(enabledPluginList, pipelineFactoryOptions);
|
|
624
|
+
const sandboxedPlugins = await EmDashRuntime.loadSandboxedPlugins(deps, db);
|
|
625
|
+
if (deps.config.marketplace && storage) await EmDashRuntime.loadMarketplacePlugins(db, storage, deps, sandboxedPlugins);
|
|
626
|
+
const mediaProviders = /* @__PURE__ */ new Map();
|
|
627
|
+
const mediaProviderEntries = deps.mediaProviderEntries ?? [];
|
|
628
|
+
const providerContext = {
|
|
629
|
+
db,
|
|
630
|
+
storage
|
|
631
|
+
};
|
|
632
|
+
for (const entry of mediaProviderEntries) try {
|
|
633
|
+
const provider = entry.createProvider(providerContext);
|
|
634
|
+
mediaProviders.set(entry.id, provider);
|
|
635
|
+
} catch (error) {
|
|
636
|
+
console.warn(`Failed to initialize media provider "${entry.id}":`, error);
|
|
637
|
+
}
|
|
638
|
+
await EmDashRuntime.resolveExclusiveHooks(pipeline, db, deps);
|
|
639
|
+
const emailPipeline = new EmailPipeline(pipeline);
|
|
640
|
+
if (sandboxRunner) sandboxRunner.setEmailSend((message, pluginId) => emailPipeline.send(message, pluginId));
|
|
641
|
+
const pipelineRef = { current: pipeline };
|
|
642
|
+
const invokeCronHook = async (pluginId, event) => {
|
|
643
|
+
const result = await pipelineRef.current.invokeCronHook(pluginId, event);
|
|
644
|
+
if (!result.success && result.error) throw result.error;
|
|
645
|
+
};
|
|
646
|
+
pipeline.setContextFactory({
|
|
647
|
+
db,
|
|
648
|
+
emailPipeline
|
|
649
|
+
});
|
|
650
|
+
let cronExecutor = null;
|
|
651
|
+
let cronScheduler = null;
|
|
652
|
+
try {
|
|
653
|
+
cronExecutor = new CronExecutor(db, invokeCronHook);
|
|
654
|
+
const recovered = await cronExecutor.recoverStaleLocks();
|
|
655
|
+
if (recovered > 0) console.log(`[cron] Recovered ${recovered} stale task lock(s)`);
|
|
656
|
+
if (typeof globalThis.navigator !== "undefined" && globalThis.navigator.userAgent === "Cloudflare-Workers") cronScheduler = new PiggybackScheduler(cronExecutor);
|
|
657
|
+
else cronScheduler = new NodeCronScheduler(cronExecutor);
|
|
658
|
+
cronScheduler.setSystemCleanup(async () => {
|
|
659
|
+
try {
|
|
660
|
+
await runSystemCleanup(db, storage ?? void 0);
|
|
661
|
+
} catch (error) {
|
|
662
|
+
console.error("[cleanup] System cleanup failed:", error);
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
pipeline.setContextFactory({ cronReschedule: () => cronScheduler?.reschedule() });
|
|
666
|
+
await cronScheduler.start();
|
|
667
|
+
} catch (error) {
|
|
668
|
+
console.warn("[cron] Failed to initialize cron system:", error);
|
|
669
|
+
}
|
|
670
|
+
return new EmDashRuntime(db, storage, deps.plugins, sandboxedPlugins, deps.sandboxedPluginEntries, pipeline, enabledPlugins, pluginStates, deps.config, mediaProviders, mediaProviderEntries, cronExecutor, cronScheduler, emailPipeline, allPipelinePlugins, pipelineFactoryOptions, deps, pipelineRef);
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Get a media provider by ID
|
|
674
|
+
*/
|
|
675
|
+
getMediaProvider(providerId) {
|
|
676
|
+
return this.mediaProviders.get(providerId);
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Get all media provider entries (for admin UI)
|
|
680
|
+
*/
|
|
681
|
+
getMediaProviderList() {
|
|
682
|
+
return this.mediaProviderEntries.map((e) => ({
|
|
683
|
+
id: e.id,
|
|
684
|
+
name: e.name,
|
|
685
|
+
icon: e.icon,
|
|
686
|
+
capabilities: e.capabilities
|
|
687
|
+
}));
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Get or create database instance
|
|
691
|
+
*/
|
|
692
|
+
static async getDatabase(deps) {
|
|
693
|
+
const ctx = getRequestContext();
|
|
694
|
+
if (ctx?.db) return ctx.db;
|
|
695
|
+
const dbConfig = deps.config.database;
|
|
696
|
+
if (!dbConfig) try {
|
|
697
|
+
return await getDb();
|
|
698
|
+
} catch {
|
|
699
|
+
throw new Error("EmDash database not configured. Either configure database in astro.config.mjs or use emdashLoader in live.config.ts");
|
|
700
|
+
}
|
|
701
|
+
const cacheKey = dbConfig.entrypoint;
|
|
702
|
+
const cached = dbCache.get(cacheKey);
|
|
703
|
+
if (cached) return cached;
|
|
704
|
+
if (dbInitPromise) return dbInitPromise;
|
|
705
|
+
dbInitPromise = (async () => {
|
|
706
|
+
const db = new Kysely({ dialect: deps.createDialect(dbConfig.config) });
|
|
707
|
+
await runMigrations(db);
|
|
708
|
+
try {
|
|
709
|
+
const [collectionCount, setupOption] = await Promise.all([db.selectFrom("_emdash_collections").select((eb) => eb.fn.countAll().as("count")).executeTakeFirstOrThrow(), db.selectFrom("options").select("value").where("name", "=", "emdash:setup_complete").executeTakeFirst()]);
|
|
710
|
+
const setupDone = (() => {
|
|
711
|
+
try {
|
|
712
|
+
return setupOption && JSON.parse(setupOption.value) === true;
|
|
713
|
+
} catch {
|
|
714
|
+
return false;
|
|
715
|
+
}
|
|
716
|
+
})();
|
|
717
|
+
if (collectionCount.count === 0 && !setupDone) {
|
|
718
|
+
const { applySeed } = await import("../apply-Bjfq_b4-.mjs").then((n) => n.n);
|
|
719
|
+
const { loadSeed } = await import("../load-yOOlckBj.mjs").then((n) => n.r);
|
|
720
|
+
const { validateSeed } = await import("../validate-O7PWmlnq.mjs").then((n) => n.n);
|
|
721
|
+
const seed = await loadSeed();
|
|
722
|
+
if (validateSeed(seed).valid) {
|
|
723
|
+
await applySeed(db, seed, { onConflict: "skip" });
|
|
724
|
+
console.log("Auto-seeded default collections");
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
} catch {}
|
|
728
|
+
dbCache.set(cacheKey, db);
|
|
729
|
+
return db;
|
|
730
|
+
})();
|
|
731
|
+
try {
|
|
732
|
+
return await dbInitPromise;
|
|
733
|
+
} finally {
|
|
734
|
+
dbInitPromise = null;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Get or create storage instance
|
|
739
|
+
*/
|
|
740
|
+
static getStorage(deps) {
|
|
741
|
+
const storageConfig = deps.config.storage;
|
|
742
|
+
if (!storageConfig || !deps.createStorage) return null;
|
|
743
|
+
const cacheKey = storageConfig.entrypoint;
|
|
744
|
+
const cached = storageCache.get(cacheKey);
|
|
745
|
+
if (cached) return cached;
|
|
746
|
+
const storage = deps.createStorage(storageConfig.config);
|
|
747
|
+
storageCache.set(cacheKey, storage);
|
|
748
|
+
return storage;
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Load sandboxed plugins using SandboxRunner
|
|
752
|
+
*/
|
|
753
|
+
static async loadSandboxedPlugins(deps, db) {
|
|
754
|
+
if (sandboxedPluginCache.size > 0) return sandboxedPluginCache;
|
|
755
|
+
if (!deps.sandboxEnabled || deps.sandboxedPluginEntries.length === 0) return sandboxedPluginCache;
|
|
756
|
+
if (!sandboxRunner && deps.createSandboxRunner) sandboxRunner = deps.createSandboxRunner({ db });
|
|
757
|
+
if (!sandboxRunner) return sandboxedPluginCache;
|
|
758
|
+
if (!sandboxRunner.isAvailable()) {
|
|
759
|
+
console.debug("EmDash: Sandbox runner not available (missing bindings), skipping sandbox");
|
|
760
|
+
return sandboxedPluginCache;
|
|
761
|
+
}
|
|
762
|
+
for (const entry of deps.sandboxedPluginEntries) {
|
|
763
|
+
const pluginKey = `${entry.id}:${entry.version}`;
|
|
764
|
+
if (sandboxedPluginCache.has(pluginKey)) continue;
|
|
765
|
+
try {
|
|
766
|
+
const manifest = {
|
|
767
|
+
id: entry.id,
|
|
768
|
+
version: entry.version,
|
|
769
|
+
capabilities: entry.capabilities ?? [],
|
|
770
|
+
allowedHosts: entry.allowedHosts ?? [],
|
|
771
|
+
storage: entry.storage ?? {},
|
|
772
|
+
hooks: [],
|
|
773
|
+
routes: [],
|
|
774
|
+
admin: {}
|
|
775
|
+
};
|
|
776
|
+
const plugin = await sandboxRunner.load(manifest, entry.code);
|
|
777
|
+
sandboxedPluginCache.set(pluginKey, plugin);
|
|
778
|
+
console.log(`EmDash: Loaded sandboxed plugin ${pluginKey} with capabilities: [${manifest.capabilities.join(", ")}]`);
|
|
779
|
+
} catch (error) {
|
|
780
|
+
console.error(`EmDash: Failed to load sandboxed plugin ${entry.id}:`, error);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
return sandboxedPluginCache;
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Cold-start: load marketplace-installed plugins from site-local R2 storage
|
|
787
|
+
*
|
|
788
|
+
* Queries _plugin_state for source='marketplace' rows, fetches each bundle
|
|
789
|
+
* from R2, and loads via SandboxRunner.
|
|
790
|
+
*/
|
|
791
|
+
static async loadMarketplacePlugins(db, storage, deps, cache) {
|
|
792
|
+
if (!sandboxRunner && deps.createSandboxRunner) sandboxRunner = deps.createSandboxRunner({ db });
|
|
793
|
+
if (!sandboxRunner || !sandboxRunner.isAvailable()) return;
|
|
794
|
+
try {
|
|
795
|
+
const marketplacePlugins = await new PluginStateRepository(db).getMarketplacePlugins();
|
|
796
|
+
for (const plugin of marketplacePlugins) {
|
|
797
|
+
if (plugin.status !== "active") continue;
|
|
798
|
+
const version = plugin.marketplaceVersion ?? plugin.version;
|
|
799
|
+
const pluginKey = `${plugin.pluginId}:${version}`;
|
|
800
|
+
if (cache.has(pluginKey)) continue;
|
|
801
|
+
try {
|
|
802
|
+
const bundle = await loadBundleFromR2(storage, plugin.pluginId, version);
|
|
803
|
+
if (!bundle) {
|
|
804
|
+
console.warn(`EmDash: Marketplace plugin ${plugin.pluginId}@${version} not found in R2`);
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
const loaded = await sandboxRunner.load(bundle.manifest, bundle.backendCode);
|
|
808
|
+
cache.set(pluginKey, loaded);
|
|
809
|
+
marketplacePluginKeys.add(pluginKey);
|
|
810
|
+
marketplaceManifestCache.set(plugin.pluginId, {
|
|
811
|
+
id: bundle.manifest.id,
|
|
812
|
+
version: bundle.manifest.version,
|
|
813
|
+
admin: bundle.manifest.admin
|
|
814
|
+
});
|
|
815
|
+
if (bundle.manifest.routes.length > 0) {
|
|
816
|
+
const routeMeta = /* @__PURE__ */ new Map();
|
|
817
|
+
for (const entry of bundle.manifest.routes) {
|
|
818
|
+
const normalized = normalizeManifestRoute(entry);
|
|
819
|
+
routeMeta.set(normalized.name, { public: normalized.public === true });
|
|
820
|
+
}
|
|
821
|
+
sandboxedRouteMetaCache.set(plugin.pluginId, routeMeta);
|
|
822
|
+
}
|
|
823
|
+
console.log(`EmDash: Loaded marketplace plugin ${pluginKey} with capabilities: [${bundle.manifest.capabilities.join(", ")}]`);
|
|
824
|
+
} catch (error) {
|
|
825
|
+
console.error(`EmDash: Failed to load marketplace plugin ${plugin.pluginId}:`, error);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
} catch {}
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Resolve exclusive hook selections on startup.
|
|
832
|
+
*
|
|
833
|
+
* Delegates to the shared resolveExclusiveHooks() in hooks.ts.
|
|
834
|
+
* The runtime version considers all pipeline providers as "active" since
|
|
835
|
+
* the pipeline was already built from only active/enabled plugins.
|
|
836
|
+
*/
|
|
837
|
+
static async resolveExclusiveHooks(pipeline, db, deps) {
|
|
838
|
+
if (pipeline.getRegisteredExclusiveHooks().length === 0) return;
|
|
839
|
+
let optionsRepo;
|
|
840
|
+
try {
|
|
841
|
+
optionsRepo = new OptionsRepository(db);
|
|
842
|
+
} catch {
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
const preferredHints = /* @__PURE__ */ new Map();
|
|
846
|
+
for (const entry of deps.sandboxedPluginEntries) if (entry.preferred && entry.preferred.length > 0) preferredHints.set(entry.id, entry.preferred);
|
|
847
|
+
await resolveExclusiveHooks({
|
|
848
|
+
pipeline,
|
|
849
|
+
isActive: () => true,
|
|
850
|
+
getOption: (key) => optionsRepo.get(key),
|
|
851
|
+
setOption: (key, value) => optionsRepo.set(key, value),
|
|
852
|
+
deleteOption: async (key) => {
|
|
853
|
+
await optionsRepo.delete(key);
|
|
854
|
+
},
|
|
855
|
+
preferredHints
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Build the manifest (rebuilt on each request for freshness)
|
|
860
|
+
*/
|
|
861
|
+
async getManifest() {
|
|
862
|
+
const manifestCollections = {};
|
|
863
|
+
try {
|
|
864
|
+
const registry = new SchemaRegistry(this.db);
|
|
865
|
+
const dbCollections = await registry.listCollections();
|
|
866
|
+
for (const collection of dbCollections) {
|
|
867
|
+
const collectionWithFields = await registry.getCollectionWithFields(collection.slug);
|
|
868
|
+
const fields = {};
|
|
869
|
+
if (collectionWithFields?.fields) for (const field of collectionWithFields.fields) {
|
|
870
|
+
const entry = {
|
|
871
|
+
kind: FIELD_TYPE_TO_KIND[field.type] ?? "string",
|
|
872
|
+
label: field.label,
|
|
873
|
+
required: field.required
|
|
874
|
+
};
|
|
875
|
+
if (field.widget) entry.widget = field.widget;
|
|
876
|
+
if (field.validation?.options) entry.options = field.validation.options.map((v) => ({
|
|
877
|
+
value: v,
|
|
878
|
+
label: v.charAt(0).toUpperCase() + v.slice(1)
|
|
879
|
+
}));
|
|
880
|
+
fields[field.slug] = entry;
|
|
881
|
+
}
|
|
882
|
+
manifestCollections[collection.slug] = {
|
|
883
|
+
label: collection.label,
|
|
884
|
+
labelSingular: collection.labelSingular || collection.label,
|
|
885
|
+
supports: collection.supports || [],
|
|
886
|
+
hasSeo: collection.hasSeo,
|
|
887
|
+
fields
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
} catch (error) {
|
|
891
|
+
console.debug("EmDash: Could not load database collections:", error);
|
|
892
|
+
}
|
|
893
|
+
const manifestPlugins = {};
|
|
894
|
+
for (const plugin of this.configuredPlugins) {
|
|
895
|
+
const status = this.pluginStates.get(plugin.id);
|
|
896
|
+
const enabled = status === void 0 || status === "active";
|
|
897
|
+
const hasAdminEntry = !!plugin.admin?.entry;
|
|
898
|
+
const hasAdminPages = (plugin.admin?.pages?.length ?? 0) > 0;
|
|
899
|
+
const hasWidgets = (plugin.admin?.widgets?.length ?? 0) > 0;
|
|
900
|
+
let adminMode = "none";
|
|
901
|
+
if (hasAdminEntry) adminMode = "react";
|
|
902
|
+
else if (hasAdminPages || hasWidgets) adminMode = "blocks";
|
|
903
|
+
manifestPlugins[plugin.id] = {
|
|
904
|
+
version: plugin.version,
|
|
905
|
+
enabled,
|
|
906
|
+
adminMode,
|
|
907
|
+
adminPages: plugin.admin?.pages,
|
|
908
|
+
dashboardWidgets: plugin.admin?.widgets,
|
|
909
|
+
portableTextBlocks: plugin.admin?.portableTextBlocks,
|
|
910
|
+
fieldWidgets: plugin.admin?.fieldWidgets
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
for (const entry of this.sandboxedPluginEntries) {
|
|
914
|
+
const status = this.pluginStates.get(entry.id);
|
|
915
|
+
const enabled = status === void 0 || status === "active";
|
|
916
|
+
const hasAdminPages = (entry.adminPages?.length ?? 0) > 0;
|
|
917
|
+
const hasWidgets = (entry.adminWidgets?.length ?? 0) > 0;
|
|
918
|
+
manifestPlugins[entry.id] = {
|
|
919
|
+
version: entry.version,
|
|
920
|
+
enabled,
|
|
921
|
+
sandboxed: true,
|
|
922
|
+
adminMode: hasAdminPages || hasWidgets ? "blocks" : "none",
|
|
923
|
+
adminPages: entry.adminPages,
|
|
924
|
+
dashboardWidgets: entry.adminWidgets
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
for (const [pluginId, meta] of marketplaceManifestCache) {
|
|
928
|
+
if (manifestPlugins[pluginId]) continue;
|
|
929
|
+
const enabled = this.pluginStates.get(pluginId) === "active";
|
|
930
|
+
const pages = meta.admin?.pages;
|
|
931
|
+
const widgets = meta.admin?.widgets;
|
|
932
|
+
const hasAdminPages = (pages?.length ?? 0) > 0;
|
|
933
|
+
const hasWidgets = (widgets?.length ?? 0) > 0;
|
|
934
|
+
manifestPlugins[pluginId] = {
|
|
935
|
+
version: meta.version,
|
|
936
|
+
enabled,
|
|
937
|
+
sandboxed: true,
|
|
938
|
+
adminMode: hasAdminPages || hasWidgets ? "blocks" : "none",
|
|
939
|
+
adminPages: pages,
|
|
940
|
+
dashboardWidgets: widgets
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
const manifestHash = await hashString(JSON.stringify(manifestCollections) + JSON.stringify(manifestPlugins));
|
|
944
|
+
const authMode = getAuthMode(this.config);
|
|
945
|
+
const authModeValue = authMode.type === "external" ? authMode.providerType : "passkey";
|
|
946
|
+
const { getI18nConfig, isI18nEnabled } = await import("../config-CKE8p9xM.mjs").then((n) => n.t);
|
|
947
|
+
const i18nConfig = getI18nConfig();
|
|
948
|
+
return {
|
|
949
|
+
version: "0.1.0",
|
|
950
|
+
hash: manifestHash,
|
|
951
|
+
collections: manifestCollections,
|
|
952
|
+
plugins: manifestPlugins,
|
|
953
|
+
authMode: authModeValue,
|
|
954
|
+
i18n: isI18nEnabled() && i18nConfig ? {
|
|
955
|
+
defaultLocale: i18nConfig.defaultLocale,
|
|
956
|
+
locales: i18nConfig.locales
|
|
957
|
+
} : void 0,
|
|
958
|
+
marketplace: !!this.config.marketplace
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* Invalidate the cached manifest (no-op now that we don't cache).
|
|
963
|
+
* Kept for API compatibility.
|
|
964
|
+
*/
|
|
965
|
+
invalidateManifest() {}
|
|
966
|
+
async handleContentList(collection, params) {
|
|
967
|
+
return handleContentList(this.db, collection, params);
|
|
968
|
+
}
|
|
969
|
+
async handleContentGet(collection, id, locale) {
|
|
970
|
+
return handleContentGet(this.db, collection, id, locale);
|
|
971
|
+
}
|
|
972
|
+
async handleContentGetIncludingTrashed(collection, id, locale) {
|
|
973
|
+
return handleContentGetIncludingTrashed(this.db, collection, id, locale);
|
|
974
|
+
}
|
|
975
|
+
async handleContentCreate(collection, body) {
|
|
976
|
+
let processedData = body.data;
|
|
977
|
+
if (this.hooks.hasHooks("content:beforeSave")) processedData = (await this.hooks.runContentBeforeSave(body.data, collection, true)).content;
|
|
978
|
+
processedData = await this.runSandboxedBeforeSave(processedData, collection, true);
|
|
979
|
+
processedData = await this.normalizeMediaFields(collection, processedData);
|
|
980
|
+
const result = await handleContentCreate(this.db, collection, {
|
|
981
|
+
...body,
|
|
982
|
+
data: processedData,
|
|
983
|
+
authorId: body.authorId,
|
|
984
|
+
bylines: body.bylines
|
|
985
|
+
});
|
|
986
|
+
if (result.success && result.data) this.runAfterSaveHooks(contentItemToRecord(result.data.item), collection, true);
|
|
987
|
+
return result;
|
|
988
|
+
}
|
|
989
|
+
async handleContentUpdate(collection, id, body) {
|
|
990
|
+
const { ContentRepository } = await import("../content-D6C2WsZC.mjs").then((n) => n.n);
|
|
991
|
+
const repo = new ContentRepository(this.db);
|
|
992
|
+
const resolvedItem = await repo.findByIdOrSlug(collection, id);
|
|
993
|
+
const resolvedId = resolvedItem?.id ?? id;
|
|
994
|
+
if (body._rev) {
|
|
995
|
+
if (!resolvedItem) return {
|
|
996
|
+
success: false,
|
|
997
|
+
error: {
|
|
998
|
+
code: "NOT_FOUND",
|
|
999
|
+
message: `Content item not found: ${id}`
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
const revCheck = validateRev(body._rev, resolvedItem);
|
|
1003
|
+
if (!revCheck.valid) return {
|
|
1004
|
+
success: false,
|
|
1005
|
+
error: {
|
|
1006
|
+
code: "CONFLICT",
|
|
1007
|
+
message: revCheck.message
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
const { _rev: _discardedRev, ...bodyWithoutRev } = body;
|
|
1012
|
+
let processedData = bodyWithoutRev.data;
|
|
1013
|
+
if (bodyWithoutRev.data) {
|
|
1014
|
+
if (this.hooks.hasHooks("content:beforeSave")) processedData = (await this.hooks.runContentBeforeSave(bodyWithoutRev.data, collection, false)).content;
|
|
1015
|
+
processedData = await this.runSandboxedBeforeSave(processedData, collection, false);
|
|
1016
|
+
processedData = await this.normalizeMediaFields(collection, processedData);
|
|
1017
|
+
}
|
|
1018
|
+
let usesDraftRevisions = false;
|
|
1019
|
+
if (processedData) try {
|
|
1020
|
+
if ((await this.schemaRegistry.getCollectionWithFields(collection))?.supports?.includes("revisions")) {
|
|
1021
|
+
usesDraftRevisions = true;
|
|
1022
|
+
const revisionRepo = new RevisionRepository(this.db);
|
|
1023
|
+
const existing = await repo.findById(collection, resolvedId);
|
|
1024
|
+
if (existing) {
|
|
1025
|
+
let baseData;
|
|
1026
|
+
if (existing.draftRevisionId) baseData = (await revisionRepo.findById(existing.draftRevisionId))?.data ?? existing.data;
|
|
1027
|
+
else baseData = existing.data;
|
|
1028
|
+
const mergedData = {
|
|
1029
|
+
...baseData,
|
|
1030
|
+
...processedData
|
|
1031
|
+
};
|
|
1032
|
+
if (bodyWithoutRev.slug !== void 0) mergedData._slug = bodyWithoutRev.slug;
|
|
1033
|
+
if (bodyWithoutRev.skipRevision && existing.draftRevisionId) await revisionRepo.updateData(existing.draftRevisionId, mergedData);
|
|
1034
|
+
else {
|
|
1035
|
+
const revision = await revisionRepo.create({
|
|
1036
|
+
collection,
|
|
1037
|
+
entryId: resolvedId,
|
|
1038
|
+
data: mergedData,
|
|
1039
|
+
authorId: bodyWithoutRev.authorId ?? void 0
|
|
1040
|
+
});
|
|
1041
|
+
const tableName = `ec_${collection}`;
|
|
1042
|
+
await sql`
|
|
1043
|
+
UPDATE ${sql.ref(tableName)}
|
|
1044
|
+
SET draft_revision_id = ${revision.id},
|
|
1045
|
+
updated_at = ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
1046
|
+
WHERE id = ${resolvedId}
|
|
1047
|
+
`.execute(this.db);
|
|
1048
|
+
revisionRepo.pruneOldRevisions(collection, resolvedId, 50).catch(() => {});
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
} catch {}
|
|
1053
|
+
const result = await handleContentUpdate(this.db, collection, resolvedId, {
|
|
1054
|
+
...bodyWithoutRev,
|
|
1055
|
+
data: usesDraftRevisions ? void 0 : processedData,
|
|
1056
|
+
slug: usesDraftRevisions ? void 0 : bodyWithoutRev.slug,
|
|
1057
|
+
authorId: bodyWithoutRev.authorId,
|
|
1058
|
+
bylines: bodyWithoutRev.bylines
|
|
1059
|
+
});
|
|
1060
|
+
if (result.success && result.data) this.runAfterSaveHooks(contentItemToRecord(result.data.item), collection, false);
|
|
1061
|
+
return result;
|
|
1062
|
+
}
|
|
1063
|
+
async handleContentDelete(collection, id) {
|
|
1064
|
+
if (this.hooks.hasHooks("content:beforeDelete")) {
|
|
1065
|
+
const { allowed } = await this.hooks.runContentBeforeDelete(id, collection);
|
|
1066
|
+
if (!allowed) return {
|
|
1067
|
+
success: false,
|
|
1068
|
+
error: {
|
|
1069
|
+
code: "DELETE_BLOCKED",
|
|
1070
|
+
message: "Delete blocked by plugin hook"
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
if (!await this.runSandboxedBeforeDelete(id, collection)) return {
|
|
1075
|
+
success: false,
|
|
1076
|
+
error: {
|
|
1077
|
+
code: "DELETE_BLOCKED",
|
|
1078
|
+
message: "Delete blocked by sandboxed plugin hook"
|
|
1079
|
+
}
|
|
1080
|
+
};
|
|
1081
|
+
const result = await handleContentDelete(this.db, collection, id);
|
|
1082
|
+
if (result.success) this.runAfterDeleteHooks(id, collection);
|
|
1083
|
+
return result;
|
|
1084
|
+
}
|
|
1085
|
+
async handleContentListTrashed(collection, params = {}) {
|
|
1086
|
+
return handleContentListTrashed(this.db, collection, params);
|
|
1087
|
+
}
|
|
1088
|
+
async handleContentRestore(collection, id) {
|
|
1089
|
+
return handleContentRestore(this.db, collection, id);
|
|
1090
|
+
}
|
|
1091
|
+
async handleContentPermanentDelete(collection, id) {
|
|
1092
|
+
return handleContentPermanentDelete(this.db, collection, id);
|
|
1093
|
+
}
|
|
1094
|
+
async handleContentCountTrashed(collection) {
|
|
1095
|
+
return handleContentCountTrashed(this.db, collection);
|
|
1096
|
+
}
|
|
1097
|
+
async handleContentDuplicate(collection, id, authorId) {
|
|
1098
|
+
return handleContentDuplicate(this.db, collection, id, authorId);
|
|
1099
|
+
}
|
|
1100
|
+
async handleContentPublish(collection, id) {
|
|
1101
|
+
return handleContentPublish(this.db, collection, id);
|
|
1102
|
+
}
|
|
1103
|
+
async handleContentUnpublish(collection, id) {
|
|
1104
|
+
return handleContentUnpublish(this.db, collection, id);
|
|
1105
|
+
}
|
|
1106
|
+
async handleContentSchedule(collection, id, scheduledAt) {
|
|
1107
|
+
return handleContentSchedule(this.db, collection, id, scheduledAt);
|
|
1108
|
+
}
|
|
1109
|
+
async handleContentUnschedule(collection, id) {
|
|
1110
|
+
return handleContentUnschedule(this.db, collection, id);
|
|
1111
|
+
}
|
|
1112
|
+
async handleContentCountScheduled(collection) {
|
|
1113
|
+
return handleContentCountScheduled(this.db, collection);
|
|
1114
|
+
}
|
|
1115
|
+
async handleContentDiscardDraft(collection, id) {
|
|
1116
|
+
return handleContentDiscardDraft(this.db, collection, id);
|
|
1117
|
+
}
|
|
1118
|
+
async handleContentCompare(collection, id) {
|
|
1119
|
+
return handleContentCompare(this.db, collection, id);
|
|
1120
|
+
}
|
|
1121
|
+
async handleContentTranslations(collection, id) {
|
|
1122
|
+
return handleContentTranslations(this.db, collection, id);
|
|
1123
|
+
}
|
|
1124
|
+
async handleMediaList(params) {
|
|
1125
|
+
return handleMediaList(this.db, params);
|
|
1126
|
+
}
|
|
1127
|
+
async handleMediaGet(id) {
|
|
1128
|
+
return handleMediaGet(this.db, id);
|
|
1129
|
+
}
|
|
1130
|
+
async handleMediaCreate(input) {
|
|
1131
|
+
let processedInput = input;
|
|
1132
|
+
if (this.hooks.hasHooks("media:beforeUpload")) {
|
|
1133
|
+
const hookResult = await this.hooks.runMediaBeforeUpload({
|
|
1134
|
+
name: input.filename,
|
|
1135
|
+
type: input.mimeType,
|
|
1136
|
+
size: input.size || 0
|
|
1137
|
+
});
|
|
1138
|
+
processedInput = {
|
|
1139
|
+
...input,
|
|
1140
|
+
filename: hookResult.file.name,
|
|
1141
|
+
mimeType: hookResult.file.type,
|
|
1142
|
+
size: hookResult.file.size
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
const result = await handleMediaCreate(this.db, processedInput);
|
|
1146
|
+
if (result.success && this.hooks.hasHooks("media:afterUpload")) {
|
|
1147
|
+
const item = result.data.item;
|
|
1148
|
+
const mediaItem = {
|
|
1149
|
+
id: item.id,
|
|
1150
|
+
filename: item.filename,
|
|
1151
|
+
mimeType: item.mimeType,
|
|
1152
|
+
size: item.size,
|
|
1153
|
+
url: `/media/${item.id}/${item.filename}`,
|
|
1154
|
+
createdAt: item.createdAt
|
|
1155
|
+
};
|
|
1156
|
+
this.hooks.runMediaAfterUpload(mediaItem).catch((err) => console.error("EmDash afterUpload hook error:", err));
|
|
1157
|
+
}
|
|
1158
|
+
return result;
|
|
1159
|
+
}
|
|
1160
|
+
async handleMediaUpdate(id, input) {
|
|
1161
|
+
return handleMediaUpdate(this.db, id, input);
|
|
1162
|
+
}
|
|
1163
|
+
async handleMediaDelete(id) {
|
|
1164
|
+
return handleMediaDelete(this.db, id);
|
|
1165
|
+
}
|
|
1166
|
+
async handleRevisionList(collection, entryId, params = {}) {
|
|
1167
|
+
return handleRevisionList(this.db, collection, entryId, params);
|
|
1168
|
+
}
|
|
1169
|
+
async handleRevisionGet(revisionId) {
|
|
1170
|
+
return handleRevisionGet(this.db, revisionId);
|
|
1171
|
+
}
|
|
1172
|
+
async handleRevisionRestore(revisionId, callerUserId) {
|
|
1173
|
+
return handleRevisionRestore(this.db, revisionId, callerUserId);
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* Get route metadata for a plugin route without invoking the handler.
|
|
1177
|
+
* Used by the catch-all route to decide auth before dispatch.
|
|
1178
|
+
* Returns null if the plugin or route doesn't exist.
|
|
1179
|
+
*/
|
|
1180
|
+
getPluginRouteMeta(pluginId, path) {
|
|
1181
|
+
if (!this.isPluginEnabled(pluginId)) return null;
|
|
1182
|
+
const routeKey = path.replace(LEADING_SLASH_PATTERN, "");
|
|
1183
|
+
const trustedPlugin = this.configuredPlugins.find((p) => p.id === pluginId);
|
|
1184
|
+
if (trustedPlugin) {
|
|
1185
|
+
const route = trustedPlugin.routes[routeKey];
|
|
1186
|
+
if (!route) return null;
|
|
1187
|
+
return { public: route.public === true };
|
|
1188
|
+
}
|
|
1189
|
+
const meta = sandboxedRouteMetaCache.get(pluginId);
|
|
1190
|
+
if (meta) {
|
|
1191
|
+
const routeMeta = meta.get(routeKey);
|
|
1192
|
+
if (routeMeta) return routeMeta;
|
|
1193
|
+
}
|
|
1194
|
+
if (routeKey === "admin") {
|
|
1195
|
+
const manifestMeta = marketplaceManifestCache.get(pluginId);
|
|
1196
|
+
if (manifestMeta?.admin?.pages?.length || manifestMeta?.admin?.widgets?.length) return { public: false };
|
|
1197
|
+
const entry = this.sandboxedPluginEntries.find((e) => e.id === pluginId);
|
|
1198
|
+
if (entry?.adminPages?.length || entry?.adminWidgets?.length) return { public: false };
|
|
1199
|
+
}
|
|
1200
|
+
if (this.findSandboxedPlugin(pluginId)) return { public: false };
|
|
1201
|
+
return null;
|
|
1202
|
+
}
|
|
1203
|
+
async handlePluginApiRoute(pluginId, _method, path, request) {
|
|
1204
|
+
if (!this.isPluginEnabled(pluginId)) return {
|
|
1205
|
+
success: false,
|
|
1206
|
+
error: {
|
|
1207
|
+
code: "NOT_FOUND",
|
|
1208
|
+
message: `Plugin not enabled: ${pluginId}`
|
|
1209
|
+
}
|
|
1210
|
+
};
|
|
1211
|
+
const trustedPlugin = this.configuredPlugins.find((p) => p.id === pluginId);
|
|
1212
|
+
if (trustedPlugin && this.enabledPlugins.has(trustedPlugin.id)) {
|
|
1213
|
+
const routeRegistry = new PluginRouteRegistry({ db: this.db });
|
|
1214
|
+
routeRegistry.register(trustedPlugin);
|
|
1215
|
+
const routeKey = path.replace(LEADING_SLASH_PATTERN, "");
|
|
1216
|
+
let body = void 0;
|
|
1217
|
+
try {
|
|
1218
|
+
body = await request.json();
|
|
1219
|
+
} catch {}
|
|
1220
|
+
return routeRegistry.invoke(pluginId, routeKey, {
|
|
1221
|
+
request,
|
|
1222
|
+
body
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
const sandboxedPlugin = this.findSandboxedPlugin(pluginId);
|
|
1226
|
+
if (sandboxedPlugin) return this.handleSandboxedRoute(sandboxedPlugin, path, request);
|
|
1227
|
+
return {
|
|
1228
|
+
success: false,
|
|
1229
|
+
error: {
|
|
1230
|
+
code: "NOT_FOUND",
|
|
1231
|
+
message: `Plugin not found: ${pluginId}`
|
|
1232
|
+
}
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
findSandboxedPlugin(pluginId) {
|
|
1236
|
+
for (const [key, plugin] of this.sandboxedPlugins) if (key.startsWith(pluginId + ":")) return plugin;
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Normalize image/file fields in content data.
|
|
1240
|
+
* Fills missing dimensions, storageKey, mimeType, and filename from providers.
|
|
1241
|
+
*/
|
|
1242
|
+
async normalizeMediaFields(collection, data) {
|
|
1243
|
+
let collectionInfo;
|
|
1244
|
+
try {
|
|
1245
|
+
collectionInfo = await this.schemaRegistry.getCollectionWithFields(collection);
|
|
1246
|
+
} catch {
|
|
1247
|
+
return data;
|
|
1248
|
+
}
|
|
1249
|
+
if (!collectionInfo?.fields) return data;
|
|
1250
|
+
const imageFields = collectionInfo.fields.filter((f) => f.type === "image" || f.type === "file");
|
|
1251
|
+
if (imageFields.length === 0) return data;
|
|
1252
|
+
const getProvider = (id) => this.getMediaProvider(id);
|
|
1253
|
+
const result = { ...data };
|
|
1254
|
+
for (const field of imageFields) {
|
|
1255
|
+
const value = result[field.slug];
|
|
1256
|
+
if (value == null) continue;
|
|
1257
|
+
try {
|
|
1258
|
+
const normalized = await normalizeMediaValue(value, getProvider);
|
|
1259
|
+
if (normalized) result[field.slug] = normalized;
|
|
1260
|
+
} catch {}
|
|
1261
|
+
}
|
|
1262
|
+
return result;
|
|
1263
|
+
}
|
|
1264
|
+
async runSandboxedBeforeSave(content, collection, isNew) {
|
|
1265
|
+
let result = content;
|
|
1266
|
+
for (const [pluginKey, plugin] of this.sandboxedPlugins) {
|
|
1267
|
+
const [id] = pluginKey.split(":");
|
|
1268
|
+
if (!id || !this.isPluginEnabled(id)) continue;
|
|
1269
|
+
try {
|
|
1270
|
+
const hookResult = await plugin.invokeHook("content:beforeSave", {
|
|
1271
|
+
content: result,
|
|
1272
|
+
collection,
|
|
1273
|
+
isNew
|
|
1274
|
+
});
|
|
1275
|
+
if (hookResult && typeof hookResult === "object" && !Array.isArray(hookResult)) {
|
|
1276
|
+
const record = {};
|
|
1277
|
+
for (const [k, v] of Object.entries(hookResult)) record[k] = v;
|
|
1278
|
+
result = record;
|
|
1279
|
+
}
|
|
1280
|
+
} catch (error) {
|
|
1281
|
+
console.error(`EmDash: Sandboxed plugin ${id} beforeSave hook error:`, error);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
return result;
|
|
1285
|
+
}
|
|
1286
|
+
async runSandboxedBeforeDelete(id, collection) {
|
|
1287
|
+
for (const [pluginKey, plugin] of this.sandboxedPlugins) {
|
|
1288
|
+
const [pluginId] = pluginKey.split(":");
|
|
1289
|
+
if (!pluginId || !this.isPluginEnabled(pluginId)) continue;
|
|
1290
|
+
try {
|
|
1291
|
+
if (await plugin.invokeHook("content:beforeDelete", {
|
|
1292
|
+
id,
|
|
1293
|
+
collection
|
|
1294
|
+
}) === false) return false;
|
|
1295
|
+
} catch (error) {
|
|
1296
|
+
console.error(`EmDash: Sandboxed plugin ${pluginId} beforeDelete hook error:`, error);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
return true;
|
|
1300
|
+
}
|
|
1301
|
+
runAfterSaveHooks(content, collection, isNew) {
|
|
1302
|
+
if (this.hooks.hasHooks("content:afterSave")) this.hooks.runContentAfterSave(content, collection, isNew).catch((err) => console.error("EmDash afterSave hook error:", err));
|
|
1303
|
+
for (const [pluginKey, plugin] of this.sandboxedPlugins) {
|
|
1304
|
+
const [id] = pluginKey.split(":");
|
|
1305
|
+
if (!id || !this.isPluginEnabled(id)) continue;
|
|
1306
|
+
plugin.invokeHook("content:afterSave", {
|
|
1307
|
+
content,
|
|
1308
|
+
collection,
|
|
1309
|
+
isNew
|
|
1310
|
+
}).catch((err) => console.error(`EmDash: Sandboxed plugin ${id} afterSave error:`, err));
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
runAfterDeleteHooks(id, collection) {
|
|
1314
|
+
if (this.hooks.hasHooks("content:afterDelete")) this.hooks.runContentAfterDelete(id, collection).catch((err) => console.error("EmDash afterDelete hook error:", err));
|
|
1315
|
+
for (const [pluginKey, plugin] of this.sandboxedPlugins) {
|
|
1316
|
+
const [pluginId] = pluginKey.split(":");
|
|
1317
|
+
if (!pluginId || !this.isPluginEnabled(pluginId)) continue;
|
|
1318
|
+
plugin.invokeHook("content:afterDelete", {
|
|
1319
|
+
id,
|
|
1320
|
+
collection
|
|
1321
|
+
}).catch((err) => console.error(`EmDash: Sandboxed plugin ${pluginId} afterDelete error:`, err));
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
async handleSandboxedRoute(plugin, path, request) {
|
|
1325
|
+
const routeName = path.replace(LEADING_SLASH_PATTERN, "");
|
|
1326
|
+
let body = void 0;
|
|
1327
|
+
try {
|
|
1328
|
+
body = await request.json();
|
|
1329
|
+
} catch {}
|
|
1330
|
+
try {
|
|
1331
|
+
const headers = sanitizeHeadersForSandbox(request.headers);
|
|
1332
|
+
const meta = extractRequestMeta(request);
|
|
1333
|
+
return {
|
|
1334
|
+
success: true,
|
|
1335
|
+
data: await plugin.invokeRoute(routeName, body, {
|
|
1336
|
+
url: request.url,
|
|
1337
|
+
method: request.method,
|
|
1338
|
+
headers,
|
|
1339
|
+
meta
|
|
1340
|
+
})
|
|
1341
|
+
};
|
|
1342
|
+
} catch (error) {
|
|
1343
|
+
console.error(`EmDash: Sandboxed plugin route error:`, error);
|
|
1344
|
+
return {
|
|
1345
|
+
success: false,
|
|
1346
|
+
error: {
|
|
1347
|
+
code: "ROUTE_ERROR",
|
|
1348
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1349
|
+
}
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* Cache for page contributions. Uses a WeakMap keyed on the PublicPageContext
|
|
1355
|
+
* object so results are collected once per page context per request, even when
|
|
1356
|
+
* multiple render components (EmDashHead, EmDashBodyStart, EmDashBodyEnd)
|
|
1357
|
+
* request contributions from the same page.
|
|
1358
|
+
*/
|
|
1359
|
+
pageContributionCache = /* @__PURE__ */ new WeakMap();
|
|
1360
|
+
/**
|
|
1361
|
+
* Collect all page contributions (metadata + fragments) in a single pass.
|
|
1362
|
+
* Results are cached by page context object identity.
|
|
1363
|
+
*/
|
|
1364
|
+
async collectPageContributions(page) {
|
|
1365
|
+
const cached = this.pageContributionCache.get(page);
|
|
1366
|
+
if (cached) return cached;
|
|
1367
|
+
const promise = this.doCollectPageContributions(page);
|
|
1368
|
+
this.pageContributionCache.set(page, promise);
|
|
1369
|
+
return promise;
|
|
1370
|
+
}
|
|
1371
|
+
async doCollectPageContributions(page) {
|
|
1372
|
+
const metadata = [];
|
|
1373
|
+
const fragments = [];
|
|
1374
|
+
if (this.hooks.hasHooks("page:metadata")) {
|
|
1375
|
+
const results = await this.hooks.runPageMetadata({ page });
|
|
1376
|
+
for (const r of results) metadata.push(...r.contributions);
|
|
1377
|
+
}
|
|
1378
|
+
if (this.hooks.hasHooks("page:fragments")) {
|
|
1379
|
+
const results = await this.hooks.runPageFragments({ page });
|
|
1380
|
+
for (const r of results) fragments.push(...r.contributions);
|
|
1381
|
+
}
|
|
1382
|
+
for (const [pluginKey, plugin] of this.sandboxedPlugins) {
|
|
1383
|
+
const [id] = pluginKey.split(":");
|
|
1384
|
+
if (!id || !this.isPluginEnabled(id)) continue;
|
|
1385
|
+
try {
|
|
1386
|
+
const result = await plugin.invokeHook("page:metadata", { page });
|
|
1387
|
+
if (result != null) {
|
|
1388
|
+
const items = Array.isArray(result) ? result : [result];
|
|
1389
|
+
for (const item of items) if (isValidMetadataContribution(item)) metadata.push(item);
|
|
1390
|
+
}
|
|
1391
|
+
} catch (error) {
|
|
1392
|
+
console.error(`EmDash: Sandboxed plugin ${id} page:metadata error:`, error);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
return {
|
|
1396
|
+
metadata,
|
|
1397
|
+
fragments
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
/**
|
|
1401
|
+
* Collect page metadata contributions from trusted and sandboxed plugins.
|
|
1402
|
+
* Delegates to the single-pass collector and returns the metadata portion.
|
|
1403
|
+
*/
|
|
1404
|
+
async collectPageMetadata(page) {
|
|
1405
|
+
const { metadata } = await this.collectPageContributions(page);
|
|
1406
|
+
return metadata;
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* Collect page fragment contributions from trusted plugins only.
|
|
1410
|
+
* Delegates to the single-pass collector and returns the fragments portion.
|
|
1411
|
+
*/
|
|
1412
|
+
async collectPageFragments(page) {
|
|
1413
|
+
const { fragments } = await this.collectPageContributions(page);
|
|
1414
|
+
return fragments;
|
|
1415
|
+
}
|
|
1416
|
+
isPluginEnabled(pluginId) {
|
|
1417
|
+
const status = this.pluginStates.get(pluginId);
|
|
1418
|
+
return status === void 0 || status === "active";
|
|
1419
|
+
}
|
|
1420
|
+
};
|
|
1421
|
+
|
|
1422
|
+
//#endregion
|
|
1423
|
+
//#region src/astro/middleware.ts
|
|
1424
|
+
/**
|
|
1425
|
+
* EmDash middleware
|
|
1426
|
+
*
|
|
1427
|
+
* Thin wrapper that initializes EmDashRuntime and attaches it to locals.
|
|
1428
|
+
* All heavy lifting happens in EmDashRuntime.
|
|
1429
|
+
*/
|
|
1430
|
+
let runtimeInstance = null;
|
|
1431
|
+
let runtimeInitializing = false;
|
|
1432
|
+
/** Whether i18n config has been initialized from the virtual module */
|
|
1433
|
+
let i18nInitialized = false;
|
|
1434
|
+
/**
|
|
1435
|
+
* Get EmDash configuration from virtual module
|
|
1436
|
+
*/
|
|
1437
|
+
function getConfig() {
|
|
1438
|
+
if (virtualConfig && typeof virtualConfig === "object") {
|
|
1439
|
+
if (!i18nInitialized) {
|
|
1440
|
+
i18nInitialized = true;
|
|
1441
|
+
const config = virtualConfig;
|
|
1442
|
+
if (config.i18n && typeof config.i18n === "object") setI18nConfig(config.i18n);
|
|
1443
|
+
else setI18nConfig(null);
|
|
1444
|
+
}
|
|
1445
|
+
return virtualConfig;
|
|
1446
|
+
}
|
|
1447
|
+
return null;
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* Get plugins from virtual module
|
|
1451
|
+
*/
|
|
1452
|
+
function getPlugins() {
|
|
1453
|
+
return plugins || [];
|
|
1454
|
+
}
|
|
1455
|
+
/**
|
|
1456
|
+
* Build runtime dependencies from virtual modules
|
|
1457
|
+
*/
|
|
1458
|
+
function buildDependencies(config) {
|
|
1459
|
+
return {
|
|
1460
|
+
config,
|
|
1461
|
+
plugins: getPlugins(),
|
|
1462
|
+
createDialect,
|
|
1463
|
+
createStorage,
|
|
1464
|
+
sandboxEnabled,
|
|
1465
|
+
sandboxedPluginEntries: sandboxedPlugins || [],
|
|
1466
|
+
createSandboxRunner,
|
|
1467
|
+
mediaProviderEntries: mediaProviders || []
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
/**
|
|
1471
|
+
* Get or create the runtime instance
|
|
1472
|
+
*/
|
|
1473
|
+
async function getRuntime(config) {
|
|
1474
|
+
if (runtimeInstance) return runtimeInstance;
|
|
1475
|
+
if (runtimeInitializing) {
|
|
1476
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1477
|
+
return getRuntime(config);
|
|
1478
|
+
}
|
|
1479
|
+
runtimeInitializing = true;
|
|
1480
|
+
try {
|
|
1481
|
+
const deps = buildDependencies(config);
|
|
1482
|
+
const runtime = await EmDashRuntime.create(deps);
|
|
1483
|
+
runtimeInstance = runtime;
|
|
1484
|
+
return runtime;
|
|
1485
|
+
} finally {
|
|
1486
|
+
runtimeInitializing = false;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
/**
|
|
1490
|
+
* Baseline security headers applied to all responses.
|
|
1491
|
+
* Admin routes get additional headers (strict CSP) from auth middleware.
|
|
1492
|
+
*/
|
|
1493
|
+
function setBaselineSecurityHeaders(response) {
|
|
1494
|
+
response.headers.set("X-Content-Type-Options", "nosniff");
|
|
1495
|
+
response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
1496
|
+
response.headers.set("Permissions-Policy", "camera=(), microphone=(), geolocation=(), payment=()");
|
|
1497
|
+
if (!response.headers.has("Content-Security-Policy")) response.headers.set("X-Frame-Options", "SAMEORIGIN");
|
|
1498
|
+
}
|
|
1499
|
+
/** Public routes that require the runtime (sitemap, robots.txt, etc.) */
|
|
1500
|
+
const PUBLIC_RUNTIME_ROUTES = new Set(["/sitemap.xml", "/robots.txt"]);
|
|
1501
|
+
const onRequest = defineMiddleware(async (context, next) => {
|
|
1502
|
+
const { request, locals, cookies } = context;
|
|
1503
|
+
const url = context.url;
|
|
1504
|
+
const isEmDashRoute = url.pathname.startsWith("/_emdash");
|
|
1505
|
+
const isPublicRuntimeRoute = PUBLIC_RUNTIME_ROUTES.has(url.pathname);
|
|
1506
|
+
const hasEditCookie = cookies.get("emdash-edit-mode")?.value === "true";
|
|
1507
|
+
const hasPreviewToken = url.searchParams.has("_preview");
|
|
1508
|
+
const playgroundDb = locals.__playgroundDb;
|
|
1509
|
+
if (!isEmDashRoute && !isPublicRuntimeRoute && !hasEditCookie && !hasPreviewToken) {
|
|
1510
|
+
if (!await context.session?.get("user") && !playgroundDb) {
|
|
1511
|
+
const response = await next();
|
|
1512
|
+
setBaselineSecurityHeaders(response);
|
|
1513
|
+
return response;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
const config = getConfig();
|
|
1517
|
+
if (!config) {
|
|
1518
|
+
console.error("EmDash: No configuration found");
|
|
1519
|
+
return next();
|
|
1520
|
+
}
|
|
1521
|
+
const doInit = async () => {
|
|
1522
|
+
try {
|
|
1523
|
+
const runtime = await getRuntime(config);
|
|
1524
|
+
locals.emdashManifest = await runtime.getManifest();
|
|
1525
|
+
locals.emdash = {
|
|
1526
|
+
handleContentList: runtime.handleContentList.bind(runtime),
|
|
1527
|
+
handleContentGet: runtime.handleContentGet.bind(runtime),
|
|
1528
|
+
handleContentCreate: runtime.handleContentCreate.bind(runtime),
|
|
1529
|
+
handleContentUpdate: runtime.handleContentUpdate.bind(runtime),
|
|
1530
|
+
handleContentDelete: runtime.handleContentDelete.bind(runtime),
|
|
1531
|
+
handleContentListTrashed: runtime.handleContentListTrashed.bind(runtime),
|
|
1532
|
+
handleContentRestore: runtime.handleContentRestore.bind(runtime),
|
|
1533
|
+
handleContentPermanentDelete: runtime.handleContentPermanentDelete.bind(runtime),
|
|
1534
|
+
handleContentCountTrashed: runtime.handleContentCountTrashed.bind(runtime),
|
|
1535
|
+
handleContentGetIncludingTrashed: runtime.handleContentGetIncludingTrashed.bind(runtime),
|
|
1536
|
+
handleContentDuplicate: runtime.handleContentDuplicate.bind(runtime),
|
|
1537
|
+
handleContentPublish: runtime.handleContentPublish.bind(runtime),
|
|
1538
|
+
handleContentUnpublish: runtime.handleContentUnpublish.bind(runtime),
|
|
1539
|
+
handleContentSchedule: runtime.handleContentSchedule.bind(runtime),
|
|
1540
|
+
handleContentUnschedule: runtime.handleContentUnschedule.bind(runtime),
|
|
1541
|
+
handleContentCountScheduled: runtime.handleContentCountScheduled.bind(runtime),
|
|
1542
|
+
handleContentDiscardDraft: runtime.handleContentDiscardDraft.bind(runtime),
|
|
1543
|
+
handleContentCompare: runtime.handleContentCompare.bind(runtime),
|
|
1544
|
+
handleContentTranslations: runtime.handleContentTranslations.bind(runtime),
|
|
1545
|
+
handleMediaList: runtime.handleMediaList.bind(runtime),
|
|
1546
|
+
handleMediaGet: runtime.handleMediaGet.bind(runtime),
|
|
1547
|
+
handleMediaCreate: runtime.handleMediaCreate.bind(runtime),
|
|
1548
|
+
handleMediaUpdate: runtime.handleMediaUpdate.bind(runtime),
|
|
1549
|
+
handleMediaDelete: runtime.handleMediaDelete.bind(runtime),
|
|
1550
|
+
handleRevisionList: runtime.handleRevisionList.bind(runtime),
|
|
1551
|
+
handleRevisionGet: runtime.handleRevisionGet.bind(runtime),
|
|
1552
|
+
handleRevisionRestore: runtime.handleRevisionRestore.bind(runtime),
|
|
1553
|
+
handlePluginApiRoute: runtime.handlePluginApiRoute.bind(runtime),
|
|
1554
|
+
getPluginRouteMeta: runtime.getPluginRouteMeta.bind(runtime),
|
|
1555
|
+
getMediaProvider: runtime.getMediaProvider.bind(runtime),
|
|
1556
|
+
getMediaProviderList: runtime.getMediaProviderList.bind(runtime),
|
|
1557
|
+
storage: runtime.storage,
|
|
1558
|
+
db: runtime.db,
|
|
1559
|
+
hooks: runtime.hooks,
|
|
1560
|
+
email: runtime.email,
|
|
1561
|
+
configuredPlugins: runtime.configuredPlugins,
|
|
1562
|
+
config,
|
|
1563
|
+
invalidateManifest: runtime.invalidateManifest.bind(runtime),
|
|
1564
|
+
getSandboxRunner: runtime.getSandboxRunner.bind(runtime),
|
|
1565
|
+
syncMarketplacePlugins: runtime.syncMarketplacePlugins.bind(runtime),
|
|
1566
|
+
setPluginStatus: runtime.setPluginStatus.bind(runtime)
|
|
1567
|
+
};
|
|
1568
|
+
} catch (error) {
|
|
1569
|
+
console.error("EmDash middleware error:", error);
|
|
1570
|
+
}
|
|
1571
|
+
const dbConfig = config?.database?.config;
|
|
1572
|
+
if (dbConfig && typeof isSessionEnabled === "function" && isSessionEnabled(dbConfig) && typeof getD1Binding === "function" && createSessionDialect) {
|
|
1573
|
+
const d1Binding = getD1Binding(dbConfig);
|
|
1574
|
+
if (d1Binding && typeof d1Binding === "object" && "withSession" in d1Binding) {
|
|
1575
|
+
const isAuthenticated = !!await context.session?.get("user");
|
|
1576
|
+
const isWrite = request.method !== "GET" && request.method !== "HEAD";
|
|
1577
|
+
const configConstraint = getDefaultConstraint(dbConfig);
|
|
1578
|
+
const cookieName = getBookmarkCookieName(dbConfig);
|
|
1579
|
+
let constraint = configConstraint;
|
|
1580
|
+
if (isAuthenticated && isWrite) constraint = "first-primary";
|
|
1581
|
+
else if (isAuthenticated) {
|
|
1582
|
+
const bookmarkCookie = context.cookies.get(cookieName);
|
|
1583
|
+
if (bookmarkCookie?.value) constraint = bookmarkCookie.value;
|
|
1584
|
+
}
|
|
1585
|
+
const session = d1Binding.withSession.call(d1Binding, constraint);
|
|
1586
|
+
return runWithContext({
|
|
1587
|
+
editMode: false,
|
|
1588
|
+
db: new Kysely({ dialect: createSessionDialect(session) })
|
|
1589
|
+
}, async () => {
|
|
1590
|
+
const response = await next();
|
|
1591
|
+
setBaselineSecurityHeaders(response);
|
|
1592
|
+
if (isAuthenticated && session && typeof session === "object" && "getBookmark" in session) {
|
|
1593
|
+
const newBookmark = session.getBookmark.call(session);
|
|
1594
|
+
if (newBookmark) response.headers.append("Set-Cookie", `${cookieName}=${newBookmark}; Path=/; HttpOnly; SameSite=Lax; Secure`);
|
|
1595
|
+
}
|
|
1596
|
+
return response;
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
const response = await next();
|
|
1601
|
+
setBaselineSecurityHeaders(response);
|
|
1602
|
+
return response;
|
|
1603
|
+
};
|
|
1604
|
+
if (playgroundDb) return runWithContext({
|
|
1605
|
+
editMode: context.cookies.get("emdash-edit-mode")?.value === "true",
|
|
1606
|
+
db: playgroundDb
|
|
1607
|
+
}, doInit);
|
|
1608
|
+
return doInit();
|
|
1609
|
+
});
|
|
1610
|
+
|
|
1611
|
+
//#endregion
|
|
1612
|
+
export { onRequest as default, onRequest };
|
|
1613
|
+
//# sourceMappingURL=middleware.mjs.map
|