emdash 0.37.0 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/action-token-On1sot_c.mjs +26 -0
- package/dist/action-token-On1sot_c.mjs.map +1 -0
- package/dist/activation-CaMfdBt1.mjs +382 -0
- package/dist/activation-CaMfdBt1.mjs.map +1 -0
- package/dist/adapters-Cq10K4_c.d.mts +171 -0
- package/dist/adapters-Cq10K4_c.d.mts.map +1 -0
- package/dist/after-PcEvMY_6.mjs +96 -0
- package/dist/after-PcEvMY_6.mjs.map +1 -0
- package/dist/allowed-origins-1Sa-2wZK.mjs +116 -0
- package/dist/allowed-origins-1Sa-2wZK.mjs.map +1 -0
- package/dist/api/route-utils.d.mts +4 -3
- package/dist/api/route-utils.d.mts.map +1 -1
- package/dist/api/route-utils.mjs +45 -29
- package/dist/api/route-utils.mjs.map +1 -1
- package/dist/api/schemas/index.d.mts +2 -2
- package/dist/api/schemas/index.mjs +7 -6
- package/dist/api-DmjKxPCY.mjs +4831 -0
- package/dist/api-DmjKxPCY.mjs.map +1 -0
- package/dist/api-tokens-BIXItFKV.mjs +163 -0
- package/dist/api-tokens-BIXItFKV.mjs.map +1 -0
- package/dist/apply-drpfmqrR.mjs +1005 -0
- package/dist/apply-drpfmqrR.mjs.map +1 -0
- package/dist/artifact-fetch-COiMhVgk.mjs +813 -0
- package/dist/artifact-fetch-COiMhVgk.mjs.map +1 -0
- package/dist/astro/index.d.mts +14 -12
- package/dist/astro/index.d.mts.map +1 -1
- package/dist/astro/index.mjs +123 -13
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +12 -10
- package/dist/astro/middleware/auth.d.mts.map +1 -1
- package/dist/astro/middleware/auth.mjs +20 -11
- package/dist/astro/middleware/auth.mjs.map +1 -1
- package/dist/astro/middleware/media-usage-write-fence.mjs +5 -5
- package/dist/astro/middleware/redirect.mjs +11 -9
- package/dist/astro/middleware/redirect.mjs.map +1 -1
- package/dist/astro/middleware/request-context.d.mts +1 -1
- package/dist/astro/middleware/request-context.d.mts.map +1 -1
- package/dist/astro/middleware/request-context.mjs +154 -28
- package/dist/astro/middleware/request-context.mjs.map +1 -1
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.d.mts +13 -10
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +122 -3041
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +10 -9
- package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs.map +1 -1
- package/dist/astro/routes/api/admin/allowed-domains/index.mjs +10 -9
- package/dist/astro/routes/api/admin/allowed-domains/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +5 -5
- package/dist/astro/routes/api/admin/api-tokens/index.mjs +6 -6
- package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +6 -6
- package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +13 -12
- package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs.map +1 -1
- package/dist/astro/routes/api/admin/byline-fields/index.mjs +13 -12
- package/dist/astro/routes/api/admin/byline-fields/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +13 -12
- package/dist/astro/routes/api/admin/byline-fields/reorder.mjs.map +1 -1
- package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +22 -21
- package/dist/astro/routes/api/admin/bylines/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +22 -21
- package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs.map +1 -1
- package/dist/astro/routes/api/admin/bylines/index.mjs +22 -21
- package/dist/astro/routes/api/admin/bylines/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/comments/_id_/status.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/comments/_id_/status.mjs +21 -54
- package/dist/astro/routes/api/admin/comments/_id_/status.mjs.map +1 -1
- package/dist/astro/routes/api/admin/comments/_id_.mjs +8 -8
- package/dist/astro/routes/api/admin/comments/bulk.mjs +15 -14
- package/dist/astro/routes/api/admin/comments/bulk.mjs.map +1 -1
- package/dist/astro/routes/api/admin/comments/counts.mjs +8 -8
- package/dist/astro/routes/api/admin/comments/index.mjs +15 -14
- package/dist/astro/routes/api/admin/comments/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +6 -6
- package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +5 -5
- package/dist/astro/routes/api/admin/media-usage/activation.mjs +17 -16
- package/dist/astro/routes/api/admin/media-usage/activation.mjs.map +1 -1
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.mjs +19 -18
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.mjs +19 -18
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.mjs.map +1 -1
- package/dist/astro/routes/api/admin/media-usage/progress.mjs +17 -17
- package/dist/astro/routes/api/admin/media-usage/repair.mjs +21 -20
- package/dist/astro/routes/api/admin/media-usage/repair.mjs.map +1 -1
- package/dist/astro/routes/api/admin/media-usage/work/index.mjs +19 -18
- package/dist/astro/routes/api/admin/media-usage/work/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/media-usage/work/retry.mjs +19 -18
- package/dist/astro/routes/api/admin/media-usage/work/retry.mjs.map +1 -1
- package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +5 -5
- package/dist/astro/routes/api/admin/oauth-clients/index.mjs +5 -5
- package/dist/astro/routes/api/admin/plugins/_id_/disable.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +9 -66
- package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/enable.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +9 -69
- package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +66 -59
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/mcp.mjs +6 -6
- package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs +65 -59
- package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +70 -61
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/update.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +87 -63
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/index.mjs +66 -59
- package/dist/astro/routes/api/admin/plugins/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +4 -4
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +65 -59
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +85 -63
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +65 -59
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +70 -61
- package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +90 -66
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/artifact.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +9 -7
- package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/install.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/install.mjs +77 -63
- package/dist/astro/routes/api/admin/plugins/registry/install.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/verify.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/verify.mjs +67 -61
- package/dist/astro/routes/api/admin/plugins/registry/verify.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/updates.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/updates.mjs +66 -60
- package/dist/astro/routes/api/admin/plugins/updates.mjs.map +1 -1
- package/dist/astro/routes/api/admin/scheduled-policy-rejections/_collection_/_id_.d.mts +8 -0
- package/dist/astro/routes/api/admin/scheduled-policy-rejections/_collection_/_id_.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/scheduled-policy-rejections/_collection_/_id_.mjs +27 -0
- package/dist/astro/routes/api/admin/scheduled-policy-rejections/_collection_/_id_.mjs.map +1 -0
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +65 -59
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +4 -4
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +65 -59
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/users/_id_/disable.mjs +4 -4
- package/dist/astro/routes/api/admin/users/_id_/enable.mjs +3 -3
- package/dist/astro/routes/api/admin/users/_id_/index.mjs +11 -10
- package/dist/astro/routes/api/admin/users/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +5 -5
- package/dist/astro/routes/api/admin/users/index.mjs +10 -9
- package/dist/astro/routes/api/admin/users/index.mjs.map +1 -1
- package/dist/astro/routes/api/auth/dev-bypass.mjs +5 -5
- package/dist/astro/routes/api/auth/invite/accept.mjs +3 -3
- package/dist/astro/routes/api/auth/invite/complete.mjs +15 -14
- package/dist/astro/routes/api/auth/invite/complete.mjs.map +1 -1
- package/dist/astro/routes/api/auth/invite/index.mjs +12 -11
- package/dist/astro/routes/api/auth/invite/index.mjs.map +1 -1
- package/dist/astro/routes/api/auth/invite/register-options.mjs +14 -13
- package/dist/astro/routes/api/auth/invite/register-options.mjs.map +1 -1
- package/dist/astro/routes/api/auth/logout.mjs +4 -4
- package/dist/astro/routes/api/auth/magic-link/send.mjs +14 -13
- package/dist/astro/routes/api/auth/magic-link/send.mjs.map +1 -1
- package/dist/astro/routes/api/auth/magic-link/verify.mjs +4 -4
- package/dist/astro/routes/api/auth/me.mjs +11 -10
- package/dist/astro/routes/api/auth/me.mjs.map +1 -1
- package/dist/astro/routes/api/auth/mode.mjs +4 -4
- package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs +4 -4
- package/dist/astro/routes/api/auth/oauth/_provider_.mjs +2 -2
- package/dist/astro/routes/api/auth/passkey/_id_.mjs +10 -9
- package/dist/astro/routes/api/auth/passkey/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/auth/passkey/index.mjs +3 -3
- package/dist/astro/routes/api/auth/passkey/options.mjs +16 -15
- package/dist/astro/routes/api/auth/passkey/options.mjs.map +1 -1
- package/dist/astro/routes/api/auth/passkey/register/options.mjs +14 -13
- package/dist/astro/routes/api/auth/passkey/register/options.mjs.map +1 -1
- package/dist/astro/routes/api/auth/passkey/register/verify.mjs +15 -14
- package/dist/astro/routes/api/auth/passkey/register/verify.mjs.map +1 -1
- package/dist/astro/routes/api/auth/passkey/verify.mjs +15 -14
- package/dist/astro/routes/api/auth/passkey/verify.mjs.map +1 -1
- package/dist/astro/routes/api/auth/signup/complete.mjs +15 -14
- package/dist/astro/routes/api/auth/signup/complete.mjs.map +1 -1
- package/dist/astro/routes/api/auth/signup/request.mjs +14 -13
- package/dist/astro/routes/api/auth/signup/request.mjs.map +1 -1
- package/dist/astro/routes/api/auth/signup/verify.mjs +3 -3
- package/dist/astro/routes/api/comments/_collection_/_contentId_/index.d.mts.map +1 -1
- package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +15 -211
- package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs.map +1 -1
- package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs +18 -17
- package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +4 -4
- package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.d.mts.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +17 -11
- package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +5 -5
- package/dist/astro/routes/api/content/_collection_/_id_/lock.d.mts +10 -0
- package/dist/astro/routes/api/content/_collection_/_id_/lock.d.mts.map +1 -0
- package/dist/astro/routes/api/content/_collection_/_id_/lock.mjs +67 -0
- package/dist/astro/routes/api/content/_collection_/_id_/lock.mjs.map +1 -0
- package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +4 -4
- package/dist/astro/routes/api/content/_collection_/_id_/plugin-extensions/_pluginId_/_kind_/_extensionId_.d.mts +8 -0
- package/dist/astro/routes/api/content/_collection_/_id_/plugin-extensions/_pluginId_/_kind_/_extensionId_.d.mts.map +1 -0
- package/dist/astro/routes/api/content/_collection_/_id_/plugin-extensions/_pluginId_/_kind_/_extensionId_.mjs +35 -0
- package/dist/astro/routes/api/content/_collection_/_id_/plugin-extensions/_pluginId_/_kind_/_extensionId_.mjs.map +1 -0
- package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +16 -15
- package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/publish.d.mts +9 -2
- package/dist/astro/routes/api/content/_collection_/_id_/publish.d.mts.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +28 -16
- package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs +27 -23
- package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs +27 -23
- package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +5 -5
- package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +4 -4
- package/dist/astro/routes/api/content/_collection_/_id_/schedule.d.mts.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +27 -12
- package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +23 -20
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +4 -4
- package/dist/astro/routes/api/content/_collection_/_id_/unpublish.d.mts.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +25 -12
- package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_.d.mts.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_.mjs +27 -14
- package/dist/astro/routes/api/content/_collection_/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/authors.mjs +4 -4
- package/dist/astro/routes/api/content/_collection_/index.mjs +18 -12
- package/dist/astro/routes/api/content/_collection_/index.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/trash.mjs +11 -10
- package/dist/astro/routes/api/content/_collection_/trash.mjs.map +1 -1
- package/dist/astro/routes/api/dashboard.mjs +14 -12
- package/dist/astro/routes/api/dashboard.mjs.map +1 -1
- package/dist/astro/routes/api/dev/emails.mjs +4 -4
- package/dist/astro/routes/api/health.d.mts +8 -0
- package/dist/astro/routes/api/health.d.mts.map +1 -0
- package/dist/astro/routes/api/health.mjs +21 -0
- package/dist/astro/routes/api/health.mjs.map +1 -0
- package/dist/astro/routes/api/import/probe.d.mts +3 -3
- package/dist/astro/routes/api/import/probe.mjs +25 -23
- package/dist/astro/routes/api/import/probe.mjs.map +1 -1
- package/dist/astro/routes/api/import/wordpress/analyze.mjs +5 -5
- package/dist/astro/routes/api/import/wordpress/execute.d.mts +13 -11
- package/dist/astro/routes/api/import/wordpress/execute.d.mts.map +1 -1
- package/dist/astro/routes/api/import/wordpress/execute.mjs +20 -19
- package/dist/astro/routes/api/import/wordpress/execute.mjs.map +1 -1
- package/dist/astro/routes/api/import/wordpress/media.d.mts +12 -10
- package/dist/astro/routes/api/import/wordpress/media.d.mts.map +1 -1
- package/dist/astro/routes/api/import/wordpress/media.mjs +15 -14
- package/dist/astro/routes/api/import/wordpress/media.mjs.map +1 -1
- package/dist/astro/routes/api/import/wordpress/prepare.mjs +14 -13
- package/dist/astro/routes/api/import/wordpress/prepare.mjs.map +1 -1
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts +13 -11
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts.map +1 -1
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +18 -17
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs.map +1 -1
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.d.mts +1 -1
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +25 -23
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs.map +1 -1
- package/dist/astro/routes/api/import/wordpress-plugin/callback.mjs +1 -1
- package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +13 -11
- package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts.map +1 -1
- package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +36 -33
- package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs.map +1 -1
- package/dist/astro/routes/api/manifest.mjs +17 -9
- package/dist/astro/routes/api/manifest.mjs.map +1 -1
- package/dist/astro/routes/api/mcp.d.mts.map +1 -1
- package/dist/astro/routes/api/mcp.mjs +192 -110
- package/dist/astro/routes/api/mcp.mjs.map +1 -1
- package/dist/astro/routes/api/media/_id_/confirm.mjs +15 -14
- package/dist/astro/routes/api/media/_id_/confirm.mjs.map +1 -1
- package/dist/astro/routes/api/media/_id_/replace.mjs +12 -11
- package/dist/astro/routes/api/media/_id_/replace.mjs.map +1 -1
- package/dist/astro/routes/api/media/_id_/upload.mjs +8 -8
- package/dist/astro/routes/api/media/_id_/usage.mjs +23 -22
- package/dist/astro/routes/api/media/_id_/usage.mjs.map +1 -1
- package/dist/astro/routes/api/media/_id_.mjs +22 -21
- package/dist/astro/routes/api/media/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/media/asset/_id_/_filename_.d.mts +8 -0
- package/dist/astro/routes/api/media/asset/_id_/_filename_.d.mts.map +1 -0
- package/dist/astro/routes/api/media/asset/_id_/_filename_.mjs +52 -0
- package/dist/astro/routes/api/media/asset/_id_/_filename_.mjs.map +1 -0
- package/dist/astro/routes/api/media/file/_...key_.mjs +3 -3
- package/dist/astro/routes/api/media/folders/_id_.mjs +12 -11
- package/dist/astro/routes/api/media/folders/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/media/folders/index.mjs +12 -11
- package/dist/astro/routes/api/media/folders/index.mjs.map +1 -1
- package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +4 -4
- package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +4 -4
- package/dist/astro/routes/api/media/providers/index.mjs +4 -4
- package/dist/astro/routes/api/media/upload-url.mjs +14 -13
- package/dist/astro/routes/api/media/upload-url.mjs.map +1 -1
- package/dist/astro/routes/api/media.mjs +26 -25
- package/dist/astro/routes/api/media.mjs.map +1 -1
- package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +15 -14
- package/dist/astro/routes/api/menus/_name_/items/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/menus/_name_/items.mjs +15 -14
- package/dist/astro/routes/api/menus/_name_/items.mjs.map +1 -1
- package/dist/astro/routes/api/menus/_name_/reorder.mjs +15 -14
- package/dist/astro/routes/api/menus/_name_/reorder.mjs.map +1 -1
- package/dist/astro/routes/api/menus/_name_/translations.mjs +15 -14
- package/dist/astro/routes/api/menus/_name_/translations.mjs.map +1 -1
- package/dist/astro/routes/api/menus/_name_.mjs +15 -14
- package/dist/astro/routes/api/menus/_name_.mjs.map +1 -1
- package/dist/astro/routes/api/menus/index.mjs +15 -14
- package/dist/astro/routes/api/menus/index.mjs.map +1 -1
- package/dist/astro/routes/api/oauth/authorize.mjs +6 -6
- package/dist/astro/routes/api/oauth/device/authorize.mjs +7 -7
- package/dist/astro/routes/api/oauth/device/code.mjs +10 -10
- package/dist/astro/routes/api/oauth/device/token.mjs +9 -9
- package/dist/astro/routes/api/oauth/register.mjs +4 -4
- package/dist/astro/routes/api/oauth/token/refresh.mjs +7 -7
- package/dist/astro/routes/api/oauth/token/revoke.mjs +7 -7
- package/dist/astro/routes/api/oauth/token.mjs +7 -7
- package/dist/astro/routes/api/openapi.json.mjs +95 -24
- package/dist/astro/routes/api/openapi.json.mjs.map +1 -1
- package/dist/astro/routes/api/plugins/_pluginId_/_...path_.d.mts +2 -1
- package/dist/astro/routes/api/plugins/_pluginId_/_...path_.d.mts.map +1 -1
- package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +22 -40
- package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs.map +1 -1
- package/dist/astro/routes/api/redirects/404s/index.mjs +18 -14
- package/dist/astro/routes/api/redirects/404s/index.mjs.map +1 -1
- package/dist/astro/routes/api/redirects/404s/summary.mjs +18 -14
- package/dist/astro/routes/api/redirects/404s/summary.mjs.map +1 -1
- package/dist/astro/routes/api/redirects/_id_.d.mts.map +1 -1
- package/dist/astro/routes/api/redirects/_id_.mjs +20 -22
- package/dist/astro/routes/api/redirects/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/redirects/index.d.mts.map +1 -1
- package/dist/astro/routes/api/redirects/index.mjs +19 -19
- package/dist/astro/routes/api/redirects/index.mjs.map +1 -1
- package/dist/astro/routes/api/relations/_id_/index.mjs +27 -23
- package/dist/astro/routes/api/relations/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/relations/_id_/translations.mjs +21 -18
- package/dist/astro/routes/api/relations/_id_/translations.mjs.map +1 -1
- package/dist/astro/routes/api/relations/index.mjs +27 -23
- package/dist/astro/routes/api/relations/index.mjs.map +1 -1
- package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +4 -4
- package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +5 -5
- package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +65 -59
- package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +65 -59
- package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +65 -59
- package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +65 -59
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/index.mjs +65 -59
- package/dist/astro/routes/api/schema/collections/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/reorder.mjs +65 -59
- package/dist/astro/routes/api/schema/collections/reorder.mjs.map +1 -1
- package/dist/astro/routes/api/schema/index.mjs +16 -14
- package/dist/astro/routes/api/schema/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs +65 -59
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs.map +1 -1
- package/dist/astro/routes/api/schema/orphans/index.mjs +65 -59
- package/dist/astro/routes/api/schema/orphans/index.mjs.map +1 -1
- package/dist/astro/routes/api/search/enable.mjs +16 -14
- package/dist/astro/routes/api/search/enable.mjs.map +1 -1
- package/dist/astro/routes/api/search/index.mjs +17 -14
- package/dist/astro/routes/api/search/index.mjs.map +1 -1
- package/dist/astro/routes/api/search/rebuild.mjs +16 -14
- package/dist/astro/routes/api/search/rebuild.mjs.map +1 -1
- package/dist/astro/routes/api/search/stats.mjs +10 -9
- package/dist/astro/routes/api/search/stats.mjs.map +1 -1
- package/dist/astro/routes/api/search/suggest.mjs +15 -13
- package/dist/astro/routes/api/search/suggest.mjs.map +1 -1
- package/dist/astro/routes/api/sections/_slug_.mjs +15 -13
- package/dist/astro/routes/api/sections/_slug_.mjs.map +1 -1
- package/dist/astro/routes/api/sections/index.mjs +15 -13
- package/dist/astro/routes/api/sections/index.mjs.map +1 -1
- package/dist/astro/routes/api/settings/backups/archives/_name_.mjs +9 -8
- package/dist/astro/routes/api/settings/backups/archives/_name_.mjs.map +1 -1
- package/dist/astro/routes/api/settings/backups/archives/index.mjs +8 -7
- package/dist/astro/routes/api/settings/backups/archives/index.mjs.map +1 -1
- package/dist/astro/routes/api/settings/backups/export.mjs +9 -8
- package/dist/astro/routes/api/settings/backups/export.mjs.map +1 -1
- package/dist/astro/routes/api/settings/backups/index.mjs +10 -9
- package/dist/astro/routes/api/settings/backups/index.mjs.map +1 -1
- package/dist/astro/routes/api/settings/email.mjs +6 -6
- package/dist/astro/routes/api/settings.mjs +22 -20
- package/dist/astro/routes/api/settings.mjs.map +1 -1
- package/dist/astro/routes/api/setup/admin-verify.mjs +16 -15
- package/dist/astro/routes/api/setup/admin-verify.mjs.map +1 -1
- package/dist/astro/routes/api/setup/admin.mjs +15 -14
- package/dist/astro/routes/api/setup/admin.mjs.map +1 -1
- package/dist/astro/routes/api/setup/dev-bypass.mjs +42 -38
- package/dist/astro/routes/api/setup/dev-bypass.mjs.map +1 -1
- package/dist/astro/routes/api/setup/dev-reset.mjs +4 -4
- package/dist/astro/routes/api/setup/index.mjs +43 -38
- package/dist/astro/routes/api/setup/index.mjs.map +1 -1
- package/dist/astro/routes/api/setup/status.mjs +5 -5
- package/dist/astro/routes/api/snapshot.mjs +11 -10
- package/dist/astro/routes/api/snapshot.mjs.map +1 -1
- package/dist/astro/routes/api/taxonomies/_name_/reorder.mjs +23 -21
- package/dist/astro/routes/api/taxonomies/_name_/reorder.mjs.map +1 -1
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +23 -21
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs.map +1 -1
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +23 -21
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs.map +1 -1
- package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +23 -21
- package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs.map +1 -1
- package/dist/astro/routes/api/taxonomies/_name_/translations.mjs +22 -20
- package/dist/astro/routes/api/taxonomies/_name_/translations.mjs.map +1 -1
- package/dist/astro/routes/api/taxonomies/_name_.mjs +23 -21
- package/dist/astro/routes/api/taxonomies/_name_.mjs.map +1 -1
- package/dist/astro/routes/api/taxonomies/index.mjs +23 -21
- package/dist/astro/routes/api/taxonomies/index.mjs.map +1 -1
- package/dist/astro/routes/api/themes/preview.mjs +8 -8
- package/dist/astro/routes/api/typegen.mjs +6 -6
- package/dist/astro/routes/api/visual-editing/action-token.d.mts +8 -0
- package/dist/astro/routes/api/visual-editing/action-token.d.mts.map +1 -0
- package/dist/astro/routes/api/visual-editing/action-token.mjs +25 -0
- package/dist/astro/routes/api/visual-editing/action-token.mjs.map +1 -0
- package/dist/astro/routes/api/visual-editing/content/_collection_/_id_/publish.d.mts +8 -0
- package/dist/astro/routes/api/visual-editing/content/_collection_/_id_/publish.d.mts.map +1 -0
- package/dist/astro/routes/api/visual-editing/content/_collection_/_id_/publish.mjs +37 -0
- package/dist/astro/routes/api/visual-editing/content/_collection_/_id_/publish.mjs.map +1 -0
- package/dist/astro/routes/api/visual-editing/toolbar-labels.d.mts +8 -0
- package/dist/astro/routes/api/visual-editing/toolbar-labels.d.mts.map +1 -0
- package/dist/astro/routes/api/visual-editing/toolbar-labels.mjs +20 -0
- package/dist/astro/routes/api/visual-editing/toolbar-labels.mjs.map +1 -0
- package/dist/astro/routes/api/well-known/auth.mjs +2 -2
- package/dist/astro/routes/api/well-known/oauth-authorization-server.mjs +2 -2
- package/dist/astro/routes/api/well-known/oauth-protected-resource.mjs +2 -2
- package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +12 -11
- package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs.map +1 -1
- package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +16 -15
- package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +16 -15
- package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs.map +1 -1
- package/dist/astro/routes/api/widget-areas/_name_.mjs +10 -10
- package/dist/astro/routes/api/widget-areas/index.mjs +16 -15
- package/dist/astro/routes/api/widget-areas/index.mjs.map +1 -1
- package/dist/astro/routes/api/widget-components.mjs +4 -4
- package/dist/astro/routes/robots.txt.mjs +13 -12
- package/dist/astro/routes/robots.txt.mjs.map +1 -1
- package/dist/astro/routes/sitemap-_collection_.xml.d.mts.map +1 -1
- package/dist/astro/routes/sitemap-_collection_.xml.mjs +24 -19
- package/dist/astro/routes/sitemap-_collection_.xml.mjs.map +1 -1
- package/dist/astro/routes/sitemap.xml.mjs +14 -13
- package/dist/astro/routes/sitemap.xml.mjs.map +1 -1
- package/dist/astro/types.d.mts +79 -26
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/auth/providers/github.d.mts +1 -1
- package/dist/auth/providers/google.d.mts +1 -1
- package/dist/authoritative-records-DbqshfKD.mjs +5172 -0
- package/dist/authoritative-records-DbqshfKD.mjs.map +1 -0
- package/dist/authorize-BXpfMpp0.mjs +40 -0
- package/dist/authorize-BXpfMpp0.mjs.map +1 -0
- package/dist/backup-BsPQdwnS.mjs +246 -0
- package/dist/backup-BsPQdwnS.mjs.map +1 -0
- package/dist/base64-DvnNgP7e.mjs +63 -0
- package/dist/base64-DvnNgP7e.mjs.map +1 -0
- package/dist/byline-Doh-wt99.mjs +906 -0
- package/dist/byline-Doh-wt99.mjs.map +1 -0
- package/dist/byline-fields-CsrdvaLV.mjs +123 -0
- package/dist/byline-fields-CsrdvaLV.mjs.map +1 -0
- package/dist/byline-registry-D6lWzXk6.mjs +406 -0
- package/dist/byline-registry-D6lWzXk6.mjs.map +1 -0
- package/dist/bylines-Cop0hkoy.mjs +214 -0
- package/dist/bylines-Cop0hkoy.mjs.map +1 -0
- package/dist/bylines-kpBzHPr8.mjs +232 -0
- package/dist/bylines-kpBzHPr8.mjs.map +1 -0
- package/dist/cache-B0pOlXdM.mjs +108 -0
- package/dist/cache-B0pOlXdM.mjs.map +1 -0
- package/dist/capture-triggers-gxQxnxrJ.mjs +362 -0
- package/dist/capture-triggers-gxQxnxrJ.mjs.map +1 -0
- package/dist/challenge-store-CkRdSdd8.mjs +51 -0
- package/dist/challenge-store-CkRdSdd8.mjs.map +1 -0
- package/dist/checksum-Bsi2D9M1.mjs +162 -0
- package/dist/checksum-Bsi2D9M1.mjs.map +1 -0
- package/dist/chrome-tags-D7LGB91U.mjs +24 -0
- package/dist/chrome-tags-D7LGB91U.mjs.map +1 -0
- package/dist/chunks-BmWyou79.mjs +25 -0
- package/dist/chunks-BmWyou79.mjs.map +1 -0
- package/dist/cli/index.mjs +921 -106
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/cf-access.d.mts +1 -1
- package/dist/client/index.d.mts +17 -7
- package/dist/client/index.d.mts.map +1 -1
- package/dist/client/index.mjs +17 -12
- package/dist/client/index.mjs.map +1 -1
- package/dist/collection-deletion-C3Xixmkx.mjs +436 -0
- package/dist/collection-deletion-C3Xixmkx.mjs.map +1 -0
- package/dist/collection-slugs-cache-D9auiKmI.mjs +85 -0
- package/dist/collection-slugs-cache-D9auiKmI.mjs.map +1 -0
- package/dist/comment-DcT8qdPK.mjs +313 -0
- package/dist/comment-DcT8qdPK.mjs.map +1 -0
- package/dist/comment-reaction-DY5-y8Y5.mjs +85 -0
- package/dist/comment-reaction-DY5-y8Y5.mjs.map +1 -0
- package/dist/comments-B67VRNGw.mjs +204 -0
- package/dist/comments-B67VRNGw.mjs.map +1 -0
- package/dist/comments-BVzzt6x5.mjs +99 -0
- package/dist/comments-BVzzt6x5.mjs.map +1 -0
- package/dist/components-DIl9Qzh6.mjs +108 -0
- package/dist/components-DIl9Qzh6.mjs.map +1 -0
- package/dist/config-B2acLeC5.mjs +242 -0
- package/dist/config-B2acLeC5.mjs.map +1 -0
- package/dist/config-CGVeT8Hc.mjs +73 -0
- package/dist/config-CGVeT8Hc.mjs.map +1 -0
- package/dist/config-D3HhLON3.d.mts +39 -0
- package/dist/config-D3HhLON3.d.mts.map +1 -0
- package/dist/config-DS7e4EZ9.d.mts +6387 -0
- package/dist/config-DS7e4EZ9.d.mts.map +1 -0
- package/dist/content-DZ3B5qtA.mjs +2216 -0
- package/dist/content-DZ3B5qtA.mjs.map +1 -0
- package/dist/content-list-query-CbPdzsna.d.mts +26 -0
- package/dist/content-list-query-CbPdzsna.d.mts.map +1 -0
- package/dist/content-policy-CM4skrNi.mjs +46 -0
- package/dist/content-policy-CM4skrNi.mjs.map +1 -0
- package/dist/content-refresh-BHLHJgyt.mjs +3599 -0
- package/dist/content-refresh-BHLHJgyt.mjs.map +1 -0
- package/dist/content-repair-BVll5WNl.mjs +440 -0
- package/dist/content-repair-BVll5WNl.mjs.map +1 -0
- package/dist/context-B195kjqZ.mjs +1835 -0
- package/dist/context-B195kjqZ.mjs.map +1 -0
- package/dist/cron-BcR3RN4K.mjs +282 -0
- package/dist/cron-BcR3RN4K.mjs.map +1 -0
- package/dist/dashboard-CZD4G7k_.mjs +151 -0
- package/dist/dashboard-CZD4G7k_.mjs.map +1 -0
- package/dist/database/migration-lock.d.mts +37 -0
- package/dist/database/migration-lock.d.mts.map +1 -0
- package/dist/database/migration-lock.mjs +76 -0
- package/dist/database/migration-lock.mjs.map +1 -0
- package/dist/database/pg-migration-lock.d.mts +1 -1
- package/dist/database/pg-migration-lock.mjs +1 -1
- package/dist/database/pg-migration-lock.mjs.map +1 -1
- package/dist/datetime-normalization-PeiEdcRY.mjs +187 -0
- package/dist/datetime-normalization-PeiEdcRY.mjs.map +1 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +4 -3
- package/dist/db/index.mjs.map +1 -1
- package/dist/db/libsql-migrations.d.mts +1 -1
- package/dist/db/libsql-migrations.mjs +7 -6
- package/dist/db/libsql-migrations.mjs.map +1 -1
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/postgres-migrations.d.mts +1 -1
- package/dist/db/postgres-migrations.mjs +7 -6
- package/dist/db/postgres-migrations.mjs.map +1 -1
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite-migrations.d.mts +1 -1
- package/dist/db/sqlite-migrations.mjs +7 -6
- package/dist/db/sqlite-migrations.mjs.map +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/db/sqlite.mjs +1 -1
- package/dist/db-errors-CBD5yMro.mjs +55 -0
- package/dist/db-errors-CBD5yMro.mjs.map +1 -0
- package/dist/default-BtlxWgh9.mjs +81 -0
- package/dist/default-BtlxWgh9.mjs.map +1 -0
- package/dist/deferred-tasks-Ba9fphlE.d.mts +9 -0
- package/dist/deferred-tasks-Ba9fphlE.d.mts.map +1 -0
- package/dist/dev-typegen-CZaPLl3V.mjs +29 -0
- package/dist/dev-typegen-CZaPLl3V.mjs.map +1 -0
- package/dist/device-flow-3E5l0fpH.mjs +466 -0
- package/dist/device-flow-3E5l0fpH.mjs.map +1 -0
- package/dist/dialect-helpers-CtYdsX4W.mjs +307 -0
- package/dist/dialect-helpers-CtYdsX4W.mjs.map +1 -0
- package/dist/direct-executor-ZI8E01Yt.mjs +104 -0
- package/dist/direct-executor-ZI8E01Yt.mjs.map +1 -0
- package/dist/dist-2hDLsLeq.mjs +810 -0
- package/dist/dist-2hDLsLeq.mjs.map +1 -0
- package/dist/email-console-gc-E3Eqj.mjs +50 -0
- package/dist/email-console-gc-E3Eqj.mjs.map +1 -0
- package/dist/emdash-runtime-DZGinqTL.mjs +4041 -0
- package/dist/emdash-runtime-DZGinqTL.mjs.map +1 -0
- package/dist/enrich-C2Mk0lwE.mjs +194 -0
- package/dist/enrich-C2Mk0lwE.mjs.map +1 -0
- package/dist/entry-lock-D3C99MXS.mjs +1520 -0
- package/dist/entry-lock-D3C99MXS.mjs.map +1 -0
- package/dist/entry-lock-D9rnj2Lu.mjs +163 -0
- package/dist/entry-lock-D9rnj2Lu.mjs.map +1 -0
- package/dist/entry-lock-DX7QkqEg.d.mts +3503 -0
- package/dist/entry-lock-DX7QkqEg.d.mts.map +1 -0
- package/dist/entry-locks-CO_KaoU5.mjs +167 -0
- package/dist/entry-locks-CO_KaoU5.mjs.map +1 -0
- package/dist/error-CCTCskie.mjs +90 -0
- package/dist/error-CCTCskie.mjs.map +1 -0
- package/dist/errors-W3PA8HqB.mjs +447 -0
- package/dist/errors-W3PA8HqB.mjs.map +1 -0
- package/dist/escape-DN4Qmjsl.mjs +9 -0
- package/dist/escape-DN4Qmjsl.mjs.map +1 -0
- package/dist/field-defs-cache-B337z8UJ.mjs +77 -0
- package/dist/field-defs-cache-B337z8UJ.mjs.map +1 -0
- package/dist/focal-point-Gr34o-4o.mjs +23 -0
- package/dist/focal-point-Gr34o-4o.mjs.map +1 -0
- package/dist/fts-manager-DMb0sbIQ.mjs +413 -0
- package/dist/fts-manager-DMb0sbIQ.mjs.map +1 -0
- package/dist/hash-CLVFzWvx.mjs +37 -0
- package/dist/hash-CLVFzWvx.mjs.map +1 -0
- package/dist/hook-result-KY1jq21o.d.mts +31 -0
- package/dist/hook-result-KY1jq21o.d.mts.map +1 -0
- package/dist/hreflang-BgS2FqhH.d.mts +50 -0
- package/dist/hreflang-BgS2FqhH.d.mts.map +1 -0
- package/dist/hreflang-CrJwqoeb.mjs +105 -0
- package/dist/hreflang-CrJwqoeb.mjs.map +1 -0
- package/dist/http-route-dispatch-DAKlanXb.mjs +339 -0
- package/dist/http-route-dispatch-DAKlanXb.mjs.map +1 -0
- package/dist/identity-DRGSHEEr.mjs +35 -0
- package/dist/identity-DRGSHEEr.mjs.map +1 -0
- package/dist/import-DFr5B_hC.mjs +2020 -0
- package/dist/import-DFr5B_hC.mjs.map +1 -0
- package/dist/index-CDwKDkTq.d.mts +623 -0
- package/dist/index-CDwKDkTq.d.mts.map +1 -0
- package/dist/index.d.mts +27 -22
- package/dist/index.mjs +101 -86
- package/dist/init-lock-4eRRB4Le.mjs +56 -0
- package/dist/init-lock-4eRRB4Le.mjs.map +1 -0
- package/dist/install-finalization-5Os-QZ-2.mjs +58 -0
- package/dist/install-finalization-5Os-QZ-2.mjs.map +1 -0
- package/dist/lifecycle-7e-Lt_kX.mjs +25 -0
- package/dist/lifecycle-7e-Lt_kX.mjs.map +1 -0
- package/dist/load-Cu5bvCZh.mjs +28 -0
- package/dist/load-Cu5bvCZh.mjs.map +1 -0
- package/dist/loader-t4TxaFiQ.mjs +1004 -0
- package/dist/loader-t4TxaFiQ.mjs.map +1 -0
- package/dist/manifest-builder-D31UoJCu.mjs +189 -0
- package/dist/manifest-builder-D31UoJCu.mjs.map +1 -0
- package/dist/manifest-schema-Bkh8SeHd.mjs +491 -0
- package/dist/manifest-schema-Bkh8SeHd.mjs.map +1 -0
- package/dist/media/image-endpoint.mjs +1 -1
- package/dist/media/index.d.mts +1 -1
- package/dist/media/index.mjs +4 -4
- package/dist/media/local-runtime.d.mts +14 -12
- package/dist/media/local-runtime.d.mts.map +1 -1
- package/dist/media/local-runtime.mjs +12 -11
- package/dist/media/local-runtime.mjs.map +1 -1
- package/dist/media-TBlKCwo7.mjs +334 -0
- package/dist/media-TBlKCwo7.mjs.map +1 -0
- package/dist/media-allowlist-s60eAIf6.mjs +44 -0
- package/dist/media-allowlist-s60eAIf6.mjs.map +1 -0
- package/dist/media-folders-ClIRLUQv.mjs +210 -0
- package/dist/media-folders-ClIRLUQv.mjs.map +1 -0
- package/dist/media-url-DY_ijO3B.mjs +43 -0
- package/dist/media-url-DY_ijO3B.mjs.map +1 -0
- package/dist/media-usage-C4NolTpQ.mjs +1833 -0
- package/dist/media-usage-C4NolTpQ.mjs.map +1 -0
- package/dist/media-usage-activation-CAhmb14O.mjs +78 -0
- package/dist/media-usage-activation-CAhmb14O.mjs.map +1 -0
- package/dist/media-usage-work-Cu5DqewW.mjs +586 -0
- package/dist/media-usage-work-Cu5DqewW.mjs.map +1 -0
- package/dist/media-usage-work-DJr3rOEN.mjs +152 -0
- package/dist/media-usage-work-DJr3rOEN.mjs.map +1 -0
- package/dist/media-usage-write-fence-DW5oHT7q.mjs +42 -0
- package/dist/media-usage-write-fence-DW5oHT7q.mjs.map +1 -0
- package/dist/menus-DbgU9z5Q.mjs +3732 -0
- package/dist/menus-DbgU9z5Q.mjs.map +1 -0
- package/dist/menus-Wy8m5NcH.mjs +753 -0
- package/dist/menus-Wy8m5NcH.mjs.map +1 -0
- package/dist/migrations/index.d.mts +2 -2
- package/dist/migrations/index.d.mts.map +1 -1
- package/dist/migrations/index.mjs +6 -5
- package/dist/mime-CCYRC4r1.mjs +36 -0
- package/dist/mime-CCYRC4r1.mjs.map +1 -0
- package/dist/mode-CkmOhFqj.mjs +23 -0
- package/dist/mode-CkmOhFqj.mjs.map +1 -0
- package/dist/node-sqlite-compat-vpVaILnb.mjs +46 -0
- package/dist/node-sqlite-compat-vpVaILnb.mjs.map +1 -0
- package/dist/normalize-dChXQjwS.mjs +191 -0
- package/dist/normalize-dChXQjwS.mjs.map +1 -0
- package/dist/oauth-authorization-DZ-R-hmx.mjs +274 -0
- package/dist/oauth-authorization-DZ-R-hmx.mjs.map +1 -0
- package/dist/oauth-clients-BMPA8vjl.mjs +266 -0
- package/dist/oauth-clients-BMPA8vjl.mjs.map +1 -0
- package/dist/oauth-state-store-0gIdb48-.mjs +50 -0
- package/dist/oauth-state-store-0gIdb48-.mjs.map +1 -0
- package/dist/oauth-user-lookup-ChVW4UWu.mjs +26 -0
- package/dist/oauth-user-lookup-ChVW4UWu.mjs.map +1 -0
- package/dist/object-cache/memory.d.mts +1 -1
- package/dist/object-cache-DlSVbn5Y.mjs +534 -0
- package/dist/object-cache-DlSVbn5Y.mjs.map +1 -0
- package/dist/options-D5WcorD5.mjs +200 -0
- package/dist/options-D5WcorD5.mjs.map +1 -0
- package/dist/options-DAknChnw.d.mts +77 -0
- package/dist/options-DAknChnw.d.mts.map +1 -0
- package/dist/page/index.d.mts +2 -2
- package/dist/page/index.d.mts.map +1 -1
- package/dist/page/index.mjs +13 -0
- package/dist/page/index.mjs.map +1 -1
- package/dist/parse-Cr379Av9.mjs +81 -0
- package/dist/parse-Cr379Av9.mjs.map +1 -0
- package/dist/parse-tj9XLNO_.d.mts +165 -0
- package/dist/parse-tj9XLNO_.d.mts.map +1 -0
- package/dist/passkey-config-BD-wMaCf.mjs +46 -0
- package/dist/passkey-config-BD-wMaCf.mjs.map +1 -0
- package/dist/patterns-Do3iqi7N.mjs +145 -0
- package/dist/patterns-Do3iqi7N.mjs.map +1 -0
- package/dist/placeholder-CubPFn_L.d.mts +306 -0
- package/dist/placeholder-CubPFn_L.d.mts.map +1 -0
- package/dist/plugin-test-runtime.d.mts +97 -0
- package/dist/plugin-test-runtime.d.mts.map +1 -0
- package/dist/plugin-test-runtime.mjs +105 -0
- package/dist/plugin-types.d.mts +53 -15
- package/dist/plugin-types.d.mts.map +1 -1
- package/dist/plugin-types.mjs +18 -1
- package/dist/plugin-types.mjs.map +1 -0
- package/dist/plugin-utils.d.mts +12 -10
- package/dist/plugin-utils.d.mts.map +1 -1
- package/dist/plugins/adapt-sandbox-entry.d.mts +12 -10
- package/dist/plugins/adapt-sandbox-entry.d.mts.map +1 -1
- package/dist/plugins/adapt-sandbox-entry.mjs +18 -13
- package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -1
- package/dist/plugins/host.d.mts +7 -0
- package/dist/plugins/host.mjs +45 -0
- package/dist/plugins/http-wire.d.mts +25 -0
- package/dist/plugins/http-wire.d.mts.map +1 -0
- package/dist/plugins/http-wire.mjs +148 -0
- package/dist/plugins/http-wire.mjs.map +1 -0
- package/dist/plugins/secret-redactor.d.mts +9 -0
- package/dist/plugins/secret-redactor.d.mts.map +1 -0
- package/dist/plugins/secret-redactor.mjs +51 -0
- package/dist/plugins/secret-redactor.mjs.map +1 -0
- package/dist/plugins-DHurtdXQ.mjs +277 -0
- package/dist/plugins-DHurtdXQ.mjs.map +1 -0
- package/dist/policy-lyZnjpwz.mjs +45 -0
- package/dist/policy-lyZnjpwz.mjs.map +1 -0
- package/dist/portable-text-oAwodbPS.mjs +286 -0
- package/dist/portable-text-oAwodbPS.mjs.map +1 -0
- package/dist/preview-Cup2bj8c.mjs +107 -0
- package/dist/preview-Cup2bj8c.mjs.map +1 -0
- package/dist/protocol-C1CNiKX4.d.mts +61 -0
- package/dist/protocol-C1CNiKX4.d.mts.map +1 -0
- package/dist/public-submission-Cs-pFMQK.mjs +245 -0
- package/dist/public-submission-Cs-pFMQK.mjs.map +1 -0
- package/dist/public-url-BWVHsfQv.mjs +101 -0
- package/dist/public-url-BWVHsfQv.mjs.map +1 -0
- package/dist/query-D-lxiOgn.mjs +890 -0
- package/dist/query-D-lxiOgn.mjs.map +1 -0
- package/dist/query-D7kHKOws.mjs +41 -0
- package/dist/query-D7kHKOws.mjs.map +1 -0
- package/dist/rate-limit-CqJn-w4h.mjs +120 -0
- package/dist/rate-limit-CqJn-w4h.mjs.map +1 -0
- package/dist/redirect-BrChFPIu.mjs +17 -0
- package/dist/redirect-BrChFPIu.mjs.map +1 -0
- package/dist/redirect-D2Yo-b6S.mjs +415 -0
- package/dist/redirect-D2Yo-b6S.mjs.map +1 -0
- package/dist/redirects-BCiTaIQ-.mjs +146 -0
- package/dist/redirects-BCiTaIQ-.mjs.map +1 -0
- package/dist/redirects-ByItliuu.mjs +644 -0
- package/dist/redirects-ByItliuu.mjs.map +1 -0
- package/dist/registry/testing.d.mts +14 -0
- package/dist/registry/testing.d.mts.map +1 -0
- package/dist/registry/testing.mjs +255 -0
- package/dist/registry/testing.mjs.map +1 -0
- package/dist/registry-CkuPoZs7.mjs +1222 -0
- package/dist/registry-CkuPoZs7.mjs.map +1 -0
- package/dist/relation-XTAtOpJO.mjs +334 -0
- package/dist/relation-XTAtOpJO.mjs.map +1 -0
- package/dist/relations-B4w-G5U9.mjs +309 -0
- package/dist/relations-B4w-G5U9.mjs.map +1 -0
- package/dist/relations-O63JWBRj.mjs +403 -0
- package/dist/relations-O63JWBRj.mjs.map +1 -0
- package/dist/request-cache-DaMhSQdx.mjs +104 -0
- package/dist/request-cache-DaMhSQdx.mjs.map +1 -0
- package/dist/request-context.d.mts +1 -1
- package/dist/request-meta-C8yl_sIs.mjs +141 -0
- package/dist/request-meta-C8yl_sIs.mjs.map +1 -0
- package/dist/resolve-BJIS8wKr.mjs +199 -0
- package/dist/resolve-BJIS8wKr.mjs.map +1 -0
- package/dist/route-wire-D66L2oaZ.mjs +197 -0
- package/dist/route-wire-D66L2oaZ.mjs.map +1 -0
- package/dist/routes-DYsTe-oR.mjs +321 -0
- package/dist/routes-DYsTe-oR.mjs.map +1 -0
- package/dist/runner-3gfxRsJG.d.mts +68 -0
- package/dist/runner-3gfxRsJG.d.mts.map +1 -0
- package/dist/runner-ClrleR8A.mjs +5502 -0
- package/dist/runner-ClrleR8A.mjs.map +1 -0
- package/dist/runtime.d.mts +13 -11
- package/dist/runtime.d.mts.map +1 -1
- package/dist/runtime.mjs +6 -5
- package/dist/runtime.mjs.map +1 -1
- package/dist/schema-CPhPM9Do.mjs +420 -0
- package/dist/schema-CPhPM9Do.mjs.map +1 -0
- package/dist/schema-DEERRrKS.mjs +9 -0
- package/dist/schema-DEERRrKS.mjs.map +1 -0
- package/dist/scopes-Bn8_rCSt.mjs +27 -0
- package/dist/scopes-Bn8_rCSt.mjs.map +1 -0
- package/dist/search-lami_Ota.mjs +448 -0
- package/dist/search-lami_Ota.mjs.map +1 -0
- package/dist/secrets-DNgVgz_e.mjs +341 -0
- package/dist/secrets-DNgVgz_e.mjs.map +1 -0
- package/dist/sections-BIpd4Cxf.mjs +346 -0
- package/dist/sections-BIpd4Cxf.mjs.map +1 -0
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +35 -31
- package/dist/seo/index.d.mts +3 -3
- package/dist/seo/index.d.mts.map +1 -1
- package/dist/seo/index.mjs +27 -26
- package/dist/seo/index.mjs.map +1 -1
- package/dist/seo-BtU70b7l.mjs +114 -0
- package/dist/seo-BtU70b7l.mjs.map +1 -0
- package/dist/seo-_4qmazxz.mjs +133 -0
- package/dist/seo-_4qmazxz.mjs.map +1 -0
- package/dist/service-Bpy5iTX0.mjs +150 -0
- package/dist/service-Bpy5iTX0.mjs.map +1 -0
- package/dist/session-user-BQeS2Mrb.mjs +51 -0
- package/dist/session-user-BQeS2Mrb.mjs.map +1 -0
- package/dist/settings-DmroR53o.mjs +420 -0
- package/dist/settings-DmroR53o.mjs.map +1 -0
- package/dist/settings-g2F2a3Ub.mjs +51 -0
- package/dist/settings-g2F2a3Ub.mjs.map +1 -0
- package/dist/setup-complete-BrZaXpVV.mjs +26 -0
- package/dist/setup-complete-BrZaXpVV.mjs.map +1 -0
- package/dist/setup-nonce-Cb9-884O.mjs +25 -0
- package/dist/setup-nonce-Cb9-884O.mjs.map +1 -0
- package/dist/single-flight-cache-B5O2qhO1.mjs +104 -0
- package/dist/single-flight-cache-B5O2qhO1.mjs.map +1 -0
- package/dist/site-url-DjhYCaep.mjs +16 -0
- package/dist/site-url-DjhYCaep.mjs.map +1 -0
- package/dist/slugify-T5GHH42D.mjs +17 -0
- package/dist/slugify-T5GHH42D.mjs.map +1 -0
- package/dist/snapshot-C32Xuv3o.mjs +233 -0
- package/dist/snapshot-C32Xuv3o.mjs.map +1 -0
- package/dist/ssrf-Co1GGFZz.mjs +406 -0
- package/dist/ssrf-Co1GGFZz.mjs.map +1 -0
- package/dist/state-C7gCoiHr.mjs +153 -0
- package/dist/state-C7gCoiHr.mjs.map +1 -0
- package/dist/status-DLoBmcDz.mjs +30 -0
- package/dist/status-DLoBmcDz.mjs.map +1 -0
- package/dist/storage/local.d.mts +1 -1
- package/dist/storage/local.mjs +1 -1
- package/dist/storage/s3.d.mts +1 -1
- package/dist/storage/s3.mjs +1 -1
- package/dist/storage-query-CPctdMKX.d.mts +1519 -0
- package/dist/storage-query-CPctdMKX.d.mts.map +1 -0
- package/dist/target-CUKlGrXb.mjs +27 -0
- package/dist/target-CUKlGrXb.mjs.map +1 -0
- package/dist/taxonomies-BpqpXJUE.mjs +707 -0
- package/dist/taxonomies-BpqpXJUE.mjs.map +1 -0
- package/dist/taxonomies-ClCo4IGO.mjs +899 -0
- package/dist/taxonomies-ClCo4IGO.mjs.map +1 -0
- package/dist/taxonomy-Dj1vpw5E.mjs +508 -0
- package/dist/taxonomy-Dj1vpw5E.mjs.map +1 -0
- package/dist/tokens-Ns02GAma.mjs +171 -0
- package/dist/tokens-Ns02GAma.mjs.map +1 -0
- package/dist/transaction-BcJO4Tmc.mjs +29 -0
- package/dist/transaction-BcJO4Tmc.mjs.map +1 -0
- package/dist/transport-B8P1xsqk.mjs +136 -0
- package/dist/transport-B8P1xsqk.mjs.map +1 -0
- package/dist/transport-BLHZZcD_.d.mts +42 -0
- package/dist/transport-BLHZZcD_.d.mts.map +1 -0
- package/dist/trusted-proxy-Cg7SuD15.mjs +51 -0
- package/dist/trusted-proxy-Cg7SuD15.mjs.map +1 -0
- package/dist/turnstile-DWCSh_Qc.mjs +53 -0
- package/dist/turnstile-DWCSh_Qc.mjs.map +1 -0
- package/dist/types-B3RkqvDW.mjs +156 -0
- package/dist/types-B3RkqvDW.mjs.map +1 -0
- package/dist/types-BkUg2yHx.mjs +92 -0
- package/dist/types-BkUg2yHx.mjs.map +1 -0
- package/dist/types-BtuppOTV.mjs +16 -0
- package/dist/types-BtuppOTV.mjs.map +1 -0
- package/dist/types-CCR3ivXE.d.mts +363 -0
- package/dist/types-CCR3ivXE.d.mts.map +1 -0
- package/dist/types-CSHgU_-_.mjs +18 -0
- package/dist/types-CSHgU_-_.mjs.map +1 -0
- package/dist/types-CbwqGOXm.d.mts +766 -0
- package/dist/types-CbwqGOXm.d.mts.map +1 -0
- package/dist/types-D4RGIc2I.d.mts +198 -0
- package/dist/types-D4RGIc2I.d.mts.map +1 -0
- package/dist/types-DEHidMEk.d.mts +1637 -0
- package/dist/types-DEHidMEk.d.mts.map +1 -0
- package/dist/types-DQeU2SR7.d.mts +279 -0
- package/dist/types-DQeU2SR7.d.mts.map +1 -0
- package/dist/types-DxQWoQLA.d.mts +264 -0
- package/dist/types-DxQWoQLA.d.mts.map +1 -0
- package/dist/types-FBBnGZ2Y.d.mts +198 -0
- package/dist/types-FBBnGZ2Y.d.mts.map +1 -0
- package/dist/types-ZCnlropY.mjs +69 -0
- package/dist/types-ZCnlropY.mjs.map +1 -0
- package/dist/types-_Qtyawiy.d.mts +124 -0
- package/dist/types-_Qtyawiy.d.mts.map +1 -0
- package/dist/types-gC6FAoa_.d.mts +75 -0
- package/dist/types-gC6FAoa_.d.mts.map +1 -0
- package/dist/types-k--LneIF.mjs +22 -0
- package/dist/types-k--LneIF.mjs.map +1 -0
- package/dist/upload-attempts-2xdnoKGe.mjs +27 -0
- package/dist/upload-attempts-2xdnoKGe.mjs.map +1 -0
- package/dist/url-pattern-3m3reMzr.mjs +33 -0
- package/dist/url-pattern-3m3reMzr.mjs.map +1 -0
- package/dist/user-DroRntkx.mjs +171 -0
- package/dist/user-DroRntkx.mjs.map +1 -0
- package/dist/utils-CC2oNGV8.mjs +435 -0
- package/dist/utils-CC2oNGV8.mjs.map +1 -0
- package/dist/validate-BCP9HNDn.mjs +384 -0
- package/dist/validate-BCP9HNDn.mjs.map +1 -0
- package/dist/validate-CyQ7I3xF.d.mts +410 -0
- package/dist/validate-CyQ7I3xF.d.mts.map +1 -0
- package/dist/validate-DANX1-Qi.mjs +120 -0
- package/dist/validate-DANX1-Qi.mjs.map +1 -0
- package/dist/validation-DBy6WF94.mjs +216 -0
- package/dist/validation-DBy6WF94.mjs.map +1 -0
- package/dist/version-B57E4Uo7.mjs +7 -0
- package/dist/version-B57E4Uo7.mjs.map +1 -0
- package/dist/widgets-wQ7Xcgq8.mjs +126 -0
- package/dist/widgets-wQ7Xcgq8.mjs.map +1 -0
- package/dist/wxr-taxonomies-DIHfvEDt.mjs +406 -0
- package/dist/wxr-taxonomies-DIHfvEDt.mjs.map +1 -0
- package/dist/wxr-taxonomies-DU2ZSiGc.d.mts +34 -0
- package/dist/wxr-taxonomies-DU2ZSiGc.d.mts.map +1 -0
- package/dist/zod-generator-D_yHIWfs.mjs +329 -0
- package/dist/zod-generator-D_yHIWfs.mjs.map +1 -0
- package/package.json +39 -12
- package/src/api/errors.ts +23 -0
- package/src/api/handlers/backup.ts +11 -5
- package/src/api/handlers/content.ts +226 -85
- package/src/api/handlers/dashboard.ts +31 -1
- package/src/api/handlers/entry-lock.ts +206 -0
- package/src/api/handlers/index.ts +3 -0
- package/src/api/handlers/manifest.ts +5 -6
- package/src/api/handlers/marketplace.ts +126 -34
- package/src/api/handlers/media-upload.ts +31 -4
- package/src/api/handlers/menus.ts +9 -0
- package/src/api/handlers/plugin-settings.ts +35 -3
- package/src/api/handlers/redirects.ts +221 -138
- package/src/api/handlers/registry.ts +75 -29
- package/src/api/handlers/revision.ts +16 -27
- package/src/api/handlers/seo.ts +10 -1
- package/src/api/handlers/snapshot.ts +87 -72
- package/src/api/handlers/taxonomies.ts +79 -5
- package/src/api/handlers/validation.ts +120 -19
- package/src/api/openapi/document.ts +133 -11
- package/src/api/schemas/content.ts +32 -12
- package/src/api/schemas/entry-lock.ts +57 -0
- package/src/api/schemas/index.ts +1 -0
- package/src/api/schemas/schema.ts +10 -1
- package/src/api/schemas/search.ts +4 -0
- package/src/api/types.ts +4 -0
- package/src/astro/integration/index.ts +22 -6
- package/src/astro/integration/routes.ts +89 -0
- package/src/astro/integration/runtime.ts +40 -24
- package/src/astro/integration/virtual-modules.ts +4 -0
- package/src/astro/integration/vite-config.ts +31 -0
- package/src/astro/middleware/auth.ts +25 -4
- package/src/astro/middleware/request-context.ts +28 -10
- package/src/astro/middleware.ts +33 -1
- package/src/astro/prefetch.ts +2 -2
- package/src/astro/routes/api/admin/comments/[id]/status.ts +22 -58
- package/src/astro/routes/api/admin/plugins/[id]/disable.ts +2 -15
- package/src/astro/routes/api/admin/plugins/[id]/enable.ts +2 -25
- package/src/astro/routes/api/admin/plugins/[id]/uninstall.ts +1 -0
- package/src/astro/routes/api/admin/plugins/[id]/update.ts +23 -4
- package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +12 -2
- package/src/astro/routes/api/admin/plugins/registry/[id]/uninstall.ts +1 -0
- package/src/astro/routes/api/admin/plugins/registry/[id]/update.ts +29 -10
- package/src/astro/routes/api/admin/plugins/registry/artifact.ts +8 -2
- package/src/astro/routes/api/admin/plugins/registry/install.ts +15 -4
- package/src/astro/routes/api/admin/plugins/registry/verify.ts +2 -1
- package/src/astro/routes/api/admin/plugins/updates.ts +6 -1
- package/src/astro/routes/api/admin/scheduled-policy-rejections/[collection]/[id].ts +34 -0
- package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +4 -259
- package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +10 -0
- package/src/astro/routes/api/content/[collection]/[id]/lock.ts +133 -0
- package/src/astro/routes/api/content/[collection]/[id]/plugin-extensions/[pluginId]/[kind]/[extensionId].ts +29 -0
- package/src/astro/routes/api/content/[collection]/[id]/publish.ts +21 -4
- package/src/astro/routes/api/content/[collection]/[id]/schedule.ts +30 -1
- package/src/astro/routes/api/content/[collection]/[id]/unpublish.ts +15 -1
- package/src/astro/routes/api/content/[collection]/[id].ts +27 -2
- package/src/astro/routes/api/content/[collection]/index.ts +3 -0
- package/src/astro/routes/api/health.ts +23 -0
- package/src/astro/routes/api/manifest.ts +14 -4
- package/src/astro/routes/api/mcp.ts +2 -1
- package/src/astro/routes/api/media/asset/[id]/[filename].ts +50 -0
- package/src/astro/routes/api/plugins/[pluginId]/[...path].ts +13 -81
- package/src/astro/routes/api/redirects/[id].ts +0 -3
- package/src/astro/routes/api/redirects/index.ts +0 -2
- package/src/astro/routes/api/search/index.ts +1 -0
- package/src/astro/routes/api/visual-editing/action-token.ts +36 -0
- package/src/astro/routes/api/visual-editing/content/[collection]/[id]/publish.ts +32 -0
- package/src/astro/routes/api/visual-editing/toolbar-labels.ts +16 -0
- package/src/astro/routes/sitemap-[collection].xml.ts +11 -4
- package/src/astro/types.ts +83 -12
- package/src/cli/commands/bundle-utils.ts +19 -19
- package/src/cli/commands/bundle.ts +9 -6
- package/src/cli/commands/content.ts +32 -6
- package/src/cli/commands/doctor.ts +25 -0
- package/src/cli/commands/export-seed.ts +195 -56
- package/src/cli/commands/migrate.ts +71 -23
- package/src/cli/commands/publish.ts +2 -2
- package/src/client/index.ts +34 -7
- package/src/comments/public-submission.ts +246 -0
- package/src/comments/service.ts +59 -12
- package/src/components/EmDashBodyEnd.astro +4 -1
- package/src/components/EmDashBodyStart.astro +4 -1
- package/src/components/EmDashHead.astro +36 -16
- package/src/components/Gallery.astro +18 -4
- package/src/components/InlineEditor.astro +3 -1
- package/src/components/InlinePortableTextEditor.tsx +106 -11
- package/src/components/PortableText.astro +4 -0
- package/src/components/Table.astro +147 -62
- package/src/components/widgets/Archives.astro +6 -5
- package/src/config/errors.ts +13 -0
- package/src/config/secrets.ts +25 -11
- package/src/content/converters/portable-text-to-prosemirror.ts +26 -2
- package/src/content/converters/prosemirror-to-portable-text.ts +35 -6
- package/src/content/converters/types.ts +10 -0
- package/src/content/known-fields.ts +44 -0
- package/src/database/connection.ts +5 -1
- package/src/database/content-datetime.ts +123 -0
- package/src/database/datetime-storage.ts +452 -0
- package/src/database/dialect-helpers.ts +161 -1
- package/src/database/migration-lock.ts +140 -0
- package/src/database/migrations/075_entry_edit_locks.ts +44 -0
- package/src/database/migrations/076_collection_nav_group.ts +22 -0
- package/src/database/migrations/077_plugin_storage_revisions.ts +89 -0
- package/src/database/migrations/078_menu_item_translation_groups.ts +15 -0
- package/src/database/migrations/079_datetime_normalization.ts +13 -0
- package/src/database/migrations/080_content_translation_locale_unique.ts +88 -0
- package/src/database/migrations/081_redirect_write_guards.ts +257 -0
- package/src/database/migrations/082_taxonomy_translation_locale_unique.ts +49 -0
- package/src/database/migrations/runner.ts +63 -15
- package/src/database/pg-migration-lock.ts +2 -2
- package/src/database/repositories/byline.ts +9 -0
- package/src/database/repositories/comment.ts +86 -0
- package/src/database/repositories/content.ts +560 -69
- package/src/database/repositories/entry-locks.ts +257 -0
- package/src/database/repositories/media.ts +35 -4
- package/src/database/repositories/menu.ts +1 -0
- package/src/database/repositories/options.ts +113 -7
- package/src/database/repositories/plugin-storage.ts +159 -0
- package/src/database/repositories/redirect.ts +262 -56
- package/src/database/repositories/revision.ts +116 -17
- package/src/database/repositories/taxonomy.ts +42 -0
- package/src/database/repositories/types.ts +5 -0
- package/src/database/types.ts +26 -1
- package/src/datetime-normalization.ts +311 -0
- package/src/db/node-sqlite-compat.ts +3 -2
- package/src/emdash-runtime.ts +1994 -424
- package/src/fields/image.ts +2 -0
- package/src/i18n/config.ts +6 -6
- package/src/i18n/resolve.ts +98 -15
- package/src/index.ts +101 -0
- package/src/loader.ts +49 -4
- package/src/mcp/server.ts +216 -68
- package/src/media/responsive.ts +23 -0
- package/src/media/usage/content-refresh.ts +2 -1
- package/src/media/usage/extractor.ts +50 -15
- package/src/media/usage/types.ts +1 -1
- package/src/menus/index.ts +64 -35
- package/src/migrations/direct-executor.ts +40 -2
- package/src/migrations/index.ts +1 -0
- package/src/migrations/protocol.ts +11 -1
- package/src/page/seo-contributions.ts +78 -1
- package/src/page/seo-panel.ts +47 -0
- package/src/plugin-test-runtime.ts +17 -0
- package/src/plugin-types.ts +128 -21
- package/src/plugins/adapt-sandbox-entry.ts +34 -27
- package/src/plugins/conditional-storage.ts +30 -0
- package/src/plugins/content-access.ts +170 -0
- package/src/plugins/content-policy.ts +74 -0
- package/src/plugins/context.ts +790 -166
- package/src/plugins/cron.ts +18 -10
- package/src/plugins/define-plugin.ts +55 -54
- package/src/plugins/editor-draft.ts +257 -0
- package/src/plugins/hooks.ts +136 -3
- package/src/plugins/host.ts +29 -0
- package/src/plugins/http-route-dispatch.ts +389 -0
- package/src/plugins/http-wire.ts +180 -0
- package/src/plugins/index.ts +97 -3
- package/src/plugins/install-finalization.ts +77 -0
- package/src/plugins/lifecycle.ts +47 -0
- package/src/plugins/manager.ts +5 -2
- package/src/plugins/manifest-schema.ts +314 -58
- package/src/plugins/media.ts +166 -0
- package/src/plugins/request-meta.ts +12 -2
- package/src/plugins/route-wire.ts +304 -0
- package/src/plugins/routes.ts +38 -5
- package/src/plugins/sandbox/index.ts +4 -0
- package/src/plugins/sandbox/proxy.ts +129 -0
- package/src/plugins/sandbox/types.ts +83 -7
- package/src/plugins/secret-redactor.ts +65 -0
- package/src/plugins/settings.ts +303 -0
- package/src/plugins/state.ts +26 -0
- package/src/plugins/storage-query.ts +45 -0
- package/src/plugins/storage-update.ts +96 -0
- package/src/plugins/types.ts +602 -15
- package/src/query.ts +84 -4
- package/src/registry/authoritative-records.ts +26 -0
- package/src/registry/config.ts +182 -31
- package/src/registry/testing.ts +100 -0
- package/src/registry/types.ts +12 -70
- package/src/scheduled-publish.ts +70 -3
- package/src/schema/collection-slugs-cache.ts +157 -0
- package/src/schema/index.ts +1 -0
- package/src/schema/registry.ts +64 -1
- package/src/schema/types.ts +32 -6
- package/src/schema/zod-generator.ts +5 -13
- package/src/search/fts-manager.ts +3 -3
- package/src/search/query.ts +32 -5
- package/src/search/types.ts +8 -0
- package/src/security/ssrf.ts +167 -141
- package/src/seed/apply.ts +41 -3
- package/src/seed/types.ts +13 -3
- package/src/seed/validate.ts +18 -1
- package/src/seo/hreflang.ts +10 -6
- package/src/seo/index.ts +5 -10
- package/src/seo/media-url.ts +26 -0
- package/src/settings/index.ts +53 -11
- package/src/taxonomies/term-counts.ts +22 -5
- package/src/visual-editing/action-token.ts +31 -0
- package/src/visual-editing/toolbar-bootstrap.ts +31 -6
- package/src/visual-editing/toolbar.ts +103 -12
- package/src/widgets/archives.ts +10 -9
- package/src/widgets/index.ts +11 -1
- package/dist/activation-K0kGBWuJ.mjs +0 -382
- package/dist/activation-K0kGBWuJ.mjs.map +0 -1
- package/dist/adapters-BDQwg236.d.mts +0 -171
- package/dist/adapters-BDQwg236.d.mts.map +0 -1
- package/dist/after-CBU2vkoO.mjs +0 -96
- package/dist/after-CBU2vkoO.mjs.map +0 -1
- package/dist/allowed-origins-BGglilcs.mjs +0 -116
- package/dist/allowed-origins-BGglilcs.mjs.map +0 -1
- package/dist/api-Dz6rgoa3.mjs +0 -10040
- package/dist/api-Dz6rgoa3.mjs.map +0 -1
- package/dist/api-tokens-BSsQACNe.mjs +0 -163
- package/dist/api-tokens-BSsQACNe.mjs.map +0 -1
- package/dist/apply-B4SIOxR1.mjs +0 -980
- package/dist/apply-B4SIOxR1.mjs.map +0 -1
- package/dist/authorize-Bkf_UHhD.mjs +0 -40
- package/dist/authorize-Bkf_UHhD.mjs.map +0 -1
- package/dist/backup-rwVAl9NI.mjs +0 -241
- package/dist/backup-rwVAl9NI.mjs.map +0 -1
- package/dist/base64-CvLhUKQv.mjs +0 -63
- package/dist/base64-CvLhUKQv.mjs.map +0 -1
- package/dist/bundle-C8S2Zl6U.mjs +0 -663
- package/dist/bundle-C8S2Zl6U.mjs.map +0 -1
- package/dist/byline-DuhF7NWl.mjs +0 -902
- package/dist/byline-DuhF7NWl.mjs.map +0 -1
- package/dist/byline-fields-BovY4KYM.mjs +0 -123
- package/dist/byline-fields-BovY4KYM.mjs.map +0 -1
- package/dist/byline-registry-Bpu9-MVs.mjs +0 -406
- package/dist/byline-registry-Bpu9-MVs.mjs.map +0 -1
- package/dist/bylines-CJ5nd6Lw.mjs +0 -232
- package/dist/bylines-CJ5nd6Lw.mjs.map +0 -1
- package/dist/bylines-RwBAeOF1.mjs +0 -214
- package/dist/bylines-RwBAeOF1.mjs.map +0 -1
- package/dist/cache-BF5Kvn59.mjs +0 -108
- package/dist/cache-BF5Kvn59.mjs.map +0 -1
- package/dist/capture-triggers-CNW1q3HK.mjs +0 -362
- package/dist/capture-triggers-CNW1q3HK.mjs.map +0 -1
- package/dist/challenge-store-DQN_Yrck.mjs +0 -51
- package/dist/challenge-store-DQN_Yrck.mjs.map +0 -1
- package/dist/chrome-tags-D8fiZG94.mjs +0 -24
- package/dist/chrome-tags-D8fiZG94.mjs.map +0 -1
- package/dist/chunks-C4szrD8o.mjs +0 -25
- package/dist/chunks-C4szrD8o.mjs.map +0 -1
- package/dist/collection-deletion-Bj07NHbj.mjs +0 -436
- package/dist/collection-deletion-Bj07NHbj.mjs.map +0 -1
- package/dist/comment-reaction-BhrvSHml.mjs +0 -85
- package/dist/comment-reaction-BhrvSHml.mjs.map +0 -1
- package/dist/comment-vxOzWbZN.mjs +0 -255
- package/dist/comment-vxOzWbZN.mjs.map +0 -1
- package/dist/comments-Ct4ensnf.mjs +0 -204
- package/dist/comments-Ct4ensnf.mjs.map +0 -1
- package/dist/components-DJbo1z0S.mjs +0 -108
- package/dist/components-DJbo1z0S.mjs.map +0 -1
- package/dist/config-B9eZ340f.d.mts +0 -34
- package/dist/config-B9eZ340f.d.mts.map +0 -1
- package/dist/config-C3EBstHi.mjs +0 -72
- package/dist/config-C3EBstHi.mjs.map +0 -1
- package/dist/config-it29yDLA.mjs +0 -1163
- package/dist/config-it29yDLA.mjs.map +0 -1
- package/dist/content-DjvM0T92.mjs +0 -1725
- package/dist/content-DjvM0T92.mjs.map +0 -1
- package/dist/content-list-query-Btqd4uV2.d.mts +0 -26
- package/dist/content-list-query-Btqd4uV2.d.mts.map +0 -1
- package/dist/content-refresh-Bwp-8yGy.mjs +0 -3588
- package/dist/content-refresh-Bwp-8yGy.mjs.map +0 -1
- package/dist/content-repair-Czdp7jz5.mjs +0 -440
- package/dist/content-repair-Czdp7jz5.mjs.map +0 -1
- package/dist/context-CX6gxOLW.mjs +0 -1068
- package/dist/context-CX6gxOLW.mjs.map +0 -1
- package/dist/cron-C6o7VppJ.mjs +0 -277
- package/dist/cron-C6o7VppJ.mjs.map +0 -1
- package/dist/dashboard-C2kTeeRI.mjs +0 -140
- package/dist/dashboard-C2kTeeRI.mjs.map +0 -1
- package/dist/db-errors-Cl-sKYTm.mjs +0 -55
- package/dist/db-errors-Cl-sKYTm.mjs.map +0 -1
- package/dist/default-Clra32mw.mjs +0 -81
- package/dist/default-Clra32mw.mjs.map +0 -1
- package/dist/deferred-tasks-CvDETTJO.d.mts +0 -9
- package/dist/deferred-tasks-CvDETTJO.d.mts.map +0 -1
- package/dist/dev-typegen-DzOPfsNj.mjs +0 -29
- package/dist/dev-typegen-DzOPfsNj.mjs.map +0 -1
- package/dist/device-flow-D2oCWvLJ.mjs +0 -466
- package/dist/device-flow-D2oCWvLJ.mjs.map +0 -1
- package/dist/dialect-helpers-BSbfaO3F.mjs +0 -202
- package/dist/dialect-helpers-BSbfaO3F.mjs.map +0 -1
- package/dist/direct-executor-Bfa5_ToE.mjs +0 -85
- package/dist/direct-executor-Bfa5_ToE.mjs.map +0 -1
- package/dist/dist-89xCrGqi.mjs +0 -810
- package/dist/dist-89xCrGqi.mjs.map +0 -1
- package/dist/email-console-Dzb6uGYa.mjs +0 -50
- package/dist/email-console-Dzb6uGYa.mjs.map +0 -1
- package/dist/enrich-BX0IDp4z.mjs +0 -194
- package/dist/enrich-BX0IDp4z.mjs.map +0 -1
- package/dist/error-DFrQjppn.mjs +0 -90
- package/dist/error-DFrQjppn.mjs.map +0 -1
- package/dist/errors-DsVoDUjJ.mjs +0 -427
- package/dist/errors-DsVoDUjJ.mjs.map +0 -1
- package/dist/escape-HmHTC1Is.mjs +0 -9
- package/dist/escape-HmHTC1Is.mjs.map +0 -1
- package/dist/field-defs-cache-BM4SIRqH.mjs +0 -77
- package/dist/field-defs-cache-BM4SIRqH.mjs.map +0 -1
- package/dist/focal-point-BeWFo6Xx.mjs +0 -23
- package/dist/focal-point-BeWFo6Xx.mjs.map +0 -1
- package/dist/fts-manager-Do1BlW2r.mjs +0 -413
- package/dist/fts-manager-Do1BlW2r.mjs.map +0 -1
- package/dist/hash-3JJPlHF2.mjs +0 -37
- package/dist/hash-3JJPlHF2.mjs.map +0 -1
- package/dist/hook-result-DCh2WUpe.d.mts +0 -31
- package/dist/hook-result-DCh2WUpe.d.mts.map +0 -1
- package/dist/hreflang-B6hbAuuO.d.mts +0 -48
- package/dist/hreflang-B6hbAuuO.d.mts.map +0 -1
- package/dist/hreflang-DQbPZliA.mjs +0 -102
- package/dist/hreflang-DQbPZliA.mjs.map +0 -1
- package/dist/identity-DksvEazf.mjs +0 -35
- package/dist/identity-DksvEazf.mjs.map +0 -1
- package/dist/import-CJpaBBBT.mjs +0 -2020
- package/dist/import-CJpaBBBT.mjs.map +0 -1
- package/dist/index-BJNMCWmu.d.mts +0 -6562
- package/dist/index-BJNMCWmu.d.mts.map +0 -1
- package/dist/index-Du8K7bWa.d.mts +0 -278
- package/dist/index-Du8K7bWa.d.mts.map +0 -1
- package/dist/init-lock-DJkX6Hto.mjs +0 -56
- package/dist/init-lock-DJkX6Hto.mjs.map +0 -1
- package/dist/load-CawHjTOH.mjs +0 -28
- package/dist/load-CawHjTOH.mjs.map +0 -1
- package/dist/loader-Z8vl7zTq.mjs +0 -966
- package/dist/loader-Z8vl7zTq.mjs.map +0 -1
- package/dist/manifest-builder-COtzuca2.mjs +0 -189
- package/dist/manifest-builder-COtzuca2.mjs.map +0 -1
- package/dist/manifest-schema-BNWwR8Vd.mjs +0 -301
- package/dist/manifest-schema-BNWwR8Vd.mjs.map +0 -1
- package/dist/media-BBm8Wg3f.mjs +0 -318
- package/dist/media-BBm8Wg3f.mjs.map +0 -1
- package/dist/media-BHjus86Y.mjs +0 -464
- package/dist/media-BHjus86Y.mjs.map +0 -1
- package/dist/media-allowlist-CoTBWoDp.mjs +0 -44
- package/dist/media-allowlist-CoTBWoDp.mjs.map +0 -1
- package/dist/media-folders-BGkATs8n.mjs +0 -210
- package/dist/media-folders-BGkATs8n.mjs.map +0 -1
- package/dist/media-upload-BCidR-EY.mjs +0 -147
- package/dist/media-upload-BCidR-EY.mjs.map +0 -1
- package/dist/media-url-DHoPr0hq.mjs +0 -26
- package/dist/media-url-DHoPr0hq.mjs.map +0 -1
- package/dist/media-usage-CdY5jhSR.mjs +0 -1833
- package/dist/media-usage-CdY5jhSR.mjs.map +0 -1
- package/dist/media-usage-activation-BgeMtK5C.mjs +0 -78
- package/dist/media-usage-activation-BgeMtK5C.mjs.map +0 -1
- package/dist/media-usage-work-CnGCdHxx.mjs +0 -152
- package/dist/media-usage-work-CnGCdHxx.mjs.map +0 -1
- package/dist/media-usage-work-D6D9sMCA.mjs +0 -586
- package/dist/media-usage-work-D6D9sMCA.mjs.map +0 -1
- package/dist/media-usage-write-fence-hjeD4u7s.mjs +0 -42
- package/dist/media-usage-write-fence-hjeD4u7s.mjs.map +0 -1
- package/dist/menus-6qSwTNxG.mjs +0 -4010
- package/dist/menus-6qSwTNxG.mjs.map +0 -1
- package/dist/menus-uvPLXMZf.mjs +0 -744
- package/dist/menus-uvPLXMZf.mjs.map +0 -1
- package/dist/mime-CUIG6x84.mjs +0 -36
- package/dist/mime-CUIG6x84.mjs.map +0 -1
- package/dist/mode-DN_WneRx.mjs +0 -23
- package/dist/mode-DN_WneRx.mjs.map +0 -1
- package/dist/node-sqlite-compat-BuJXIeMz.mjs +0 -46
- package/dist/node-sqlite-compat-BuJXIeMz.mjs.map +0 -1
- package/dist/normalize-C2xNjTSG.mjs +0 -191
- package/dist/normalize-C2xNjTSG.mjs.map +0 -1
- package/dist/oauth-authorization-Dj6Kwc21.mjs +0 -274
- package/dist/oauth-authorization-Dj6Kwc21.mjs.map +0 -1
- package/dist/oauth-clients-FD7I0J8m.mjs +0 -266
- package/dist/oauth-clients-FD7I0J8m.mjs.map +0 -1
- package/dist/oauth-state-store-DkmDOmHs.mjs +0 -50
- package/dist/oauth-state-store-DkmDOmHs.mjs.map +0 -1
- package/dist/oauth-user-lookup-CvIwGpYd.mjs +0 -26
- package/dist/oauth-user-lookup-CvIwGpYd.mjs.map +0 -1
- package/dist/object-cache-DVKnyOfy.mjs +0 -534
- package/dist/object-cache-DVKnyOfy.mjs.map +0 -1
- package/dist/options-BHPeIbBd.mjs +0 -117
- package/dist/options-BHPeIbBd.mjs.map +0 -1
- package/dist/options-CYb4y7hM.d.mts +0 -221
- package/dist/options-CYb4y7hM.d.mts.map +0 -1
- package/dist/parse-Ckpg-Qfp.mjs +0 -81
- package/dist/parse-Ckpg-Qfp.mjs.map +0 -1
- package/dist/passkey-config-O_lj3y0k.mjs +0 -46
- package/dist/passkey-config-O_lj3y0k.mjs.map +0 -1
- package/dist/patterns-D9iDO3j8.mjs +0 -145
- package/dist/patterns-D9iDO3j8.mjs.map +0 -1
- package/dist/placeholder-4X9BrRrp.d.mts +0 -306
- package/dist/placeholder-4X9BrRrp.d.mts.map +0 -1
- package/dist/policy-Dhtwntrt.mjs +0 -45
- package/dist/policy-Dhtwntrt.mjs.map +0 -1
- package/dist/portable-text-U85hOafW.mjs +0 -286
- package/dist/portable-text-U85hOafW.mjs.map +0 -1
- package/dist/preview-B_vJLvf8.mjs +0 -107
- package/dist/preview-B_vJLvf8.mjs.map +0 -1
- package/dist/protocol-JdD2YhY9.d.mts +0 -52
- package/dist/protocol-JdD2YhY9.d.mts.map +0 -1
- package/dist/public-url-CarhZJgf.mjs +0 -101
- package/dist/public-url-CarhZJgf.mjs.map +0 -1
- package/dist/query-BZKZcG5b.mjs +0 -881
- package/dist/query-BZKZcG5b.mjs.map +0 -1
- package/dist/query-BndaFxxp.mjs +0 -41
- package/dist/query-BndaFxxp.mjs.map +0 -1
- package/dist/rate-limit-BZmATC6W.mjs +0 -120
- package/dist/rate-limit-BZmATC6W.mjs.map +0 -1
- package/dist/redirect-CZkbozu9.mjs +0 -313
- package/dist/redirect-CZkbozu9.mjs.map +0 -1
- package/dist/redirect-CzqnLxK-.mjs +0 -17
- package/dist/redirect-CzqnLxK-.mjs.map +0 -1
- package/dist/redirects-BLNOWQTn.mjs +0 -1238
- package/dist/redirects-BLNOWQTn.mjs.map +0 -1
- package/dist/redirects-CqzNM0KM.mjs +0 -579
- package/dist/redirects-CqzNM0KM.mjs.map +0 -1
- package/dist/registry-AZAsRsor.mjs +0 -1179
- package/dist/registry-AZAsRsor.mjs.map +0 -1
- package/dist/relation-04DfKRTO.mjs +0 -334
- package/dist/relation-04DfKRTO.mjs.map +0 -1
- package/dist/relations-C4htk5sb.mjs +0 -309
- package/dist/relations-C4htk5sb.mjs.map +0 -1
- package/dist/relations-CcFwDJdJ.mjs +0 -403
- package/dist/relations-CcFwDJdJ.mjs.map +0 -1
- package/dist/relations-DnRK4YIT.d.mts +0 -3354
- package/dist/relations-DnRK4YIT.d.mts.map +0 -1
- package/dist/request-cache-DH9zdCOD.mjs +0 -104
- package/dist/request-cache-DH9zdCOD.mjs.map +0 -1
- package/dist/request-meta-DYxLlnmz.mjs +0 -140
- package/dist/request-meta-DYxLlnmz.mjs.map +0 -1
- package/dist/resolve-prZ3mP7l.mjs +0 -143
- package/dist/resolve-prZ3mP7l.mjs.map +0 -1
- package/dist/runner-BLIDKpmA.mjs +0 -4676
- package/dist/runner-BLIDKpmA.mjs.map +0 -1
- package/dist/runner-YKfIMnaF.d.mts +0 -67
- package/dist/runner-YKfIMnaF.d.mts.map +0 -1
- package/dist/schema-CGnBeLiW.mjs +0 -9
- package/dist/schema-CGnBeLiW.mjs.map +0 -1
- package/dist/schema-DYr-Vd-L.mjs +0 -420
- package/dist/schema-DYr-Vd-L.mjs.map +0 -1
- package/dist/scopes-BhWvBavj.mjs +0 -27
- package/dist/scopes-BhWvBavj.mjs.map +0 -1
- package/dist/search-C-qDk4kx.mjs +0 -439
- package/dist/search-C-qDk4kx.mjs.map +0 -1
- package/dist/secrets-rEtHt-6z.mjs +0 -330
- package/dist/secrets-rEtHt-6z.mjs.map +0 -1
- package/dist/sections-B_nYvlwn.mjs +0 -346
- package/dist/sections-B_nYvlwn.mjs.map +0 -1
- package/dist/seo-926NRIgt.mjs +0 -133
- package/dist/seo-926NRIgt.mjs.map +0 -1
- package/dist/seo-CVWkdv6x.mjs +0 -113
- package/dist/seo-CVWkdv6x.mjs.map +0 -1
- package/dist/service-CJl3ZAYs.mjs +0 -195
- package/dist/service-CJl3ZAYs.mjs.map +0 -1
- package/dist/session-user-BlMgqCHS.mjs +0 -51
- package/dist/session-user-BlMgqCHS.mjs.map +0 -1
- package/dist/settings--wobrkkC.mjs +0 -258
- package/dist/settings--wobrkkC.mjs.map +0 -1
- package/dist/settings-Cwm6o2jY.mjs +0 -51
- package/dist/settings-Cwm6o2jY.mjs.map +0 -1
- package/dist/setup-complete-DQbClIN2.mjs +0 -26
- package/dist/setup-complete-DQbClIN2.mjs.map +0 -1
- package/dist/setup-nonce-DZCFS2sn.mjs +0 -25
- package/dist/setup-nonce-DZCFS2sn.mjs.map +0 -1
- package/dist/single-flight-cache-BPuGPdcN.mjs +0 -104
- package/dist/single-flight-cache-BPuGPdcN.mjs.map +0 -1
- package/dist/site-url-BrxA_E9H.mjs +0 -16
- package/dist/site-url-BrxA_E9H.mjs.map +0 -1
- package/dist/slugify-B994zkXC.mjs +0 -17
- package/dist/slugify-B994zkXC.mjs.map +0 -1
- package/dist/snapshot--OCDTqdb.mjs +0 -216
- package/dist/snapshot--OCDTqdb.mjs.map +0 -1
- package/dist/ssrf-5sIM5jzl.mjs +0 -349
- package/dist/ssrf-5sIM5jzl.mjs.map +0 -1
- package/dist/state-BpFVsJZB.mjs +0 -136
- package/dist/state-BpFVsJZB.mjs.map +0 -1
- package/dist/status-B777-dXz.mjs +0 -30
- package/dist/status-B777-dXz.mjs.map +0 -1
- package/dist/target-BmR0iTwz.mjs +0 -27
- package/dist/target-BmR0iTwz.mjs.map +0 -1
- package/dist/taxonomies-DkAkMupr.mjs +0 -837
- package/dist/taxonomies-DkAkMupr.mjs.map +0 -1
- package/dist/taxonomies-Wfy_Lg12.mjs +0 -698
- package/dist/taxonomies-Wfy_Lg12.mjs.map +0 -1
- package/dist/taxonomy-tJepplLL.mjs +0 -484
- package/dist/taxonomy-tJepplLL.mjs.map +0 -1
- package/dist/tokens-BCO1oruF.mjs +0 -171
- package/dist/tokens-BCO1oruF.mjs.map +0 -1
- package/dist/transaction-0KOpOyMu.mjs +0 -29
- package/dist/transaction-0KOpOyMu.mjs.map +0 -1
- package/dist/transport-Dr3qtwXC.mjs +0 -136
- package/dist/transport-Dr3qtwXC.mjs.map +0 -1
- package/dist/transport-xh6YKW2G.d.mts +0 -42
- package/dist/transport-xh6YKW2G.d.mts.map +0 -1
- package/dist/trusted-proxy-BR5F1Otp.mjs +0 -51
- package/dist/trusted-proxy-BR5F1Otp.mjs.map +0 -1
- package/dist/types-3qNFPlci.mjs +0 -16
- package/dist/types-3qNFPlci.mjs.map +0 -1
- package/dist/types-75KIqzL9.mjs +0 -142
- package/dist/types-75KIqzL9.mjs.map +0 -1
- package/dist/types-B6GrPic4.d.mts +0 -1335
- package/dist/types-B6GrPic4.d.mts.map +0 -1
- package/dist/types-BuHW4xDE.d.mts +0 -259
- package/dist/types-BuHW4xDE.d.mts.map +0 -1
- package/dist/types-C00oyCZp.d.mts +0 -198
- package/dist/types-C00oyCZp.d.mts.map +0 -1
- package/dist/types-CDtkNSFQ.d.mts +0 -75
- package/dist/types-CDtkNSFQ.d.mts.map +0 -1
- package/dist/types-CPeXo1wF.d.mts +0 -124
- package/dist/types-CPeXo1wF.d.mts.map +0 -1
- package/dist/types-CQPWhPxE.mjs +0 -92
- package/dist/types-CQPWhPxE.mjs.map +0 -1
- package/dist/types-CXFkwEGC.d.mts +0 -198
- package/dist/types-CXFkwEGC.d.mts.map +0 -1
- package/dist/types-DM3vioBY.mjs +0 -18
- package/dist/types-DM3vioBY.mjs.map +0 -1
- package/dist/types-DczELMop.d.mts +0 -363
- package/dist/types-DczELMop.d.mts.map +0 -1
- package/dist/types-DfuTR6dL.mjs +0 -3
- package/dist/types-FV23hEoW.d.mts +0 -743
- package/dist/types-FV23hEoW.d.mts.map +0 -1
- package/dist/types-xDMtUFnO.d.mts +0 -257
- package/dist/types-xDMtUFnO.d.mts.map +0 -1
- package/dist/upload-attempts-BsqoQ1fn.mjs +0 -27
- package/dist/upload-attempts-BsqoQ1fn.mjs.map +0 -1
- package/dist/url-pattern-D4c2czpJ.mjs +0 -33
- package/dist/url-pattern-D4c2czpJ.mjs.map +0 -1
- package/dist/user-BTZ40aq6.mjs +0 -171
- package/dist/user-BTZ40aq6.mjs.map +0 -1
- package/dist/utils-BKvNjjhY.mjs +0 -435
- package/dist/utils-BKvNjjhY.mjs.map +0 -1
- package/dist/validate-BpwoDOgt.mjs +0 -120
- package/dist/validate-BpwoDOgt.mjs.map +0 -1
- package/dist/validate-DE_LRwo9.mjs +0 -379
- package/dist/validate-DE_LRwo9.mjs.map +0 -1
- package/dist/validate-DUP27OYD.d.mts +0 -400
- package/dist/validate-DUP27OYD.d.mts.map +0 -1
- package/dist/validation-BMImFJ11.mjs +0 -153
- package/dist/validation-BMImFJ11.mjs.map +0 -1
- package/dist/version-Bii3suaO.mjs +0 -7
- package/dist/version-Bii3suaO.mjs.map +0 -1
- package/dist/widgets-D3gJ10xU.mjs +0 -122
- package/dist/widgets-D3gJ10xU.mjs.map +0 -1
- package/dist/wxr-taxonomies-B2Og_6rU.d.mts +0 -34
- package/dist/wxr-taxonomies-B2Og_6rU.d.mts.map +0 -1
- package/dist/wxr-taxonomies-Cxngsjwo.mjs +0 -406
- package/dist/wxr-taxonomies-Cxngsjwo.mjs.map +0 -1
- package/dist/zod-generator-BuYSQJB7.mjs +0 -331
- package/dist/zod-generator-BuYSQJB7.mjs.map +0 -1
- /package/dist/{api-tokens-BfYts-sq.mjs → api-tokens-DS1VzN7_.mjs} +0 -0
- /package/dist/{ssrf-Dg3cIT-b.mjs → ssrf-BRpTDMV6.mjs} +0 -0
package/src/emdash-runtime.ts
CHANGED
|
@@ -7,16 +7,30 @@
|
|
|
7
7
|
* Created once per worker lifetime, cached and reused across requests.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { getLocaleDir, resolveLocale } from "@emdash-cms/admin/locales";
|
|
10
11
|
import { Permissions } from "@emdash-cms/auth";
|
|
11
12
|
import type { Element } from "@emdash-cms/blocks";
|
|
13
|
+
import {
|
|
14
|
+
normalizePluginPagePath,
|
|
15
|
+
validateBlockResponse,
|
|
16
|
+
validateContentEditorActionResponse,
|
|
17
|
+
type BlockValidationPolicy,
|
|
18
|
+
type PluginUiContext,
|
|
19
|
+
} from "@emdash-cms/blocks/server";
|
|
20
|
+
import { isJsonPostRouteContract } from "@emdash-cms/plugin-types";
|
|
12
21
|
import { Kysely, type Dialect } from "kysely";
|
|
13
22
|
import virtualConfig from "virtual:emdash/config";
|
|
14
23
|
import { z } from "zod";
|
|
15
24
|
|
|
16
25
|
import { ErrorCode } from "./api/errors.js";
|
|
17
26
|
import { buildManifestCollections } from "./api/handlers/manifest.js";
|
|
27
|
+
import {
|
|
28
|
+
handleMediaUpload as uploadMedia,
|
|
29
|
+
type MediaUploadInput,
|
|
30
|
+
} from "./api/handlers/media-upload.js";
|
|
18
31
|
import { assertMediaUsageActivationWriteAllowed } from "./api/media-usage-write-fence.js";
|
|
19
32
|
import { validateRev } from "./api/rev.js";
|
|
33
|
+
import { getSiteBaseUrl } from "./api/site-url.js";
|
|
20
34
|
import type {
|
|
21
35
|
EmDashConfig,
|
|
22
36
|
PluginAdminPage,
|
|
@@ -25,7 +39,9 @@ import type {
|
|
|
25
39
|
import type { EmDashManifest } from "./astro/types.js";
|
|
26
40
|
import { getAuthMode } from "./auth/mode.js";
|
|
27
41
|
import { getTrustedProxyHeaders } from "./auth/trusted-proxy.js";
|
|
42
|
+
import { lookupContentAuthor, sendCommentNotification } from "./comments/notifications.js";
|
|
28
43
|
import type { ContentFieldFilters } from "./content-list-query.js";
|
|
44
|
+
import { keepKnownFields, staleStoredKeys } from "./content/known-fields.js";
|
|
29
45
|
import { isSqlite } from "./database/dialect-helpers.js";
|
|
30
46
|
import { kyselyLogOption } from "./database/instrumentation.js";
|
|
31
47
|
import {
|
|
@@ -38,6 +54,8 @@ import {
|
|
|
38
54
|
MIGRATION_RACE_WAIT_MS,
|
|
39
55
|
} from "./database/migrations/runner.js";
|
|
40
56
|
import { AuditRepository } from "./database/repositories/audit.js";
|
|
57
|
+
import { CommentRepository } from "./database/repositories/comment.js";
|
|
58
|
+
import type { CommentStatus } from "./database/repositories/comment.js";
|
|
41
59
|
import { ContentRepository } from "./database/repositories/content.js";
|
|
42
60
|
import { RevisionRepository } from "./database/repositories/revision.js";
|
|
43
61
|
import { ContentMutationConflictError } from "./database/repositories/types.js";
|
|
@@ -46,7 +64,7 @@ import type {
|
|
|
46
64
|
ContentDateField,
|
|
47
65
|
} from "./database/repositories/types.js";
|
|
48
66
|
import type { ImageValue } from "./fields/types.js";
|
|
49
|
-
import { getI18nConfig } from "./i18n/config.js";
|
|
67
|
+
import { getI18nConfig, resolveContentCreateLocale } from "./i18n/config.js";
|
|
50
68
|
import { repairLocaleCasing } from "./i18n/repair-locale-casing.js";
|
|
51
69
|
import { warnAboutUnconfiguredTaxonomyLocales } from "./i18n/taxonomy-locale-diagnostic.js";
|
|
52
70
|
import { normalizeMediaValue } from "./media/normalize.js";
|
|
@@ -59,7 +77,17 @@ import {
|
|
|
59
77
|
refreshContentMediaUsageAfterWrite,
|
|
60
78
|
} from "./media/usage/content-refresh.js";
|
|
61
79
|
import { processMediaUsageWorkAfterWrite } from "./media/usage/work-processor.js";
|
|
62
|
-
import {
|
|
80
|
+
import {
|
|
81
|
+
scheduledPolicyRejectionKey,
|
|
82
|
+
type ScheduledPolicyRejection,
|
|
83
|
+
} from "./plugins/content-policy.js";
|
|
84
|
+
import { createCommentAccess, createTaxonomyAccessWithWrite } from "./plugins/context.js";
|
|
85
|
+
import type { ContentActionCallbacks } from "./plugins/context.js";
|
|
86
|
+
import type { PluginContentCacheInvalidator } from "./plugins/routes.js";
|
|
87
|
+
import {
|
|
88
|
+
createSandboxedPluginProxy,
|
|
89
|
+
getSandboxSaveRejectionDetails,
|
|
90
|
+
} from "./plugins/sandbox/proxy.js";
|
|
63
91
|
import { createSandboxRunnerOptions } from "./plugins/sandbox/runner-options.js";
|
|
64
92
|
import { getSandboxRouteErrorDetails } from "./plugins/sandbox/types.js";
|
|
65
93
|
import type {
|
|
@@ -68,13 +96,18 @@ import type {
|
|
|
68
96
|
SandboxRunnerFactory,
|
|
69
97
|
} from "./plugins/sandbox/types.js";
|
|
70
98
|
import type {
|
|
71
|
-
|
|
99
|
+
ActorInfo,
|
|
100
|
+
ContentActionOrigin,
|
|
101
|
+
ContentItem as PluginContentItem,
|
|
72
102
|
ResolvedPlugin,
|
|
73
103
|
MediaItem,
|
|
104
|
+
MediaMetadataPatch,
|
|
74
105
|
PluginManifest,
|
|
75
106
|
PluginCapability,
|
|
76
107
|
PluginStorageConfig,
|
|
77
108
|
PluginMcpManifestConfig,
|
|
109
|
+
PluginEditorAction,
|
|
110
|
+
PluginEditorPanel,
|
|
78
111
|
PublicPageContext,
|
|
79
112
|
PageMetadataContribution,
|
|
80
113
|
PageFragmentContribution,
|
|
@@ -82,8 +115,18 @@ import type {
|
|
|
82
115
|
FieldWidgetConfig,
|
|
83
116
|
SettingField,
|
|
84
117
|
UserInfo,
|
|
118
|
+
CollectionCommentSettings,
|
|
119
|
+
ModerationDecision,
|
|
120
|
+
PluginComment,
|
|
121
|
+
PluginCommentStatus,
|
|
122
|
+
ContentWriteInput,
|
|
123
|
+
PluginContentCreateCallback,
|
|
124
|
+
VersionedContentItem,
|
|
85
125
|
} from "./plugins/types.js";
|
|
126
|
+
import { normalizePluginCapabilities } from "./plugins/types.js";
|
|
86
127
|
import { recordSchedulerHeartbeatSafely } from "./scheduler-health.js";
|
|
128
|
+
import { primeRegisteredCollections } from "./schema/collection-slugs-cache.js";
|
|
129
|
+
import type { CollectionWithFields } from "./schema/types.js";
|
|
87
130
|
import { isMissingTableError } from "./utils/db-errors.js";
|
|
88
131
|
import { hashString } from "./utils/hash.js";
|
|
89
132
|
import { createInitLock, type InitLock, initWithLock } from "./utils/init-lock.js";
|
|
@@ -93,6 +136,19 @@ import { COMMIT, VERSION } from "./version.js";
|
|
|
93
136
|
const LEADING_SLASH_PATTERN = /^\//;
|
|
94
137
|
const LOCALE_CASING_REPAIR_OPTION = "emdash:repair_locale_casing";
|
|
95
138
|
|
|
139
|
+
const PLUGIN_COMMENT_STATUSES = new Set<string>(["approved", "pending", "spam"]);
|
|
140
|
+
|
|
141
|
+
function assertPluginCommentStatus(
|
|
142
|
+
value: string,
|
|
143
|
+
name: "status" | "expectedStatus",
|
|
144
|
+
): asserts value is PluginCommentStatus {
|
|
145
|
+
if (!PLUGIN_COMMENT_STATUSES.has(value)) {
|
|
146
|
+
throw Object.assign(new Error(`${name} must be one of: approved, pending, spam`), {
|
|
147
|
+
code: "COMMENT_STATUS_INVALID",
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
96
152
|
function getLocaleCasingRepairVersion(locales: readonly string[]): string | null {
|
|
97
153
|
if (locales.length === 0) return null;
|
|
98
154
|
return `1:${locales.toSorted().join(",")}`;
|
|
@@ -112,50 +168,20 @@ function parseStringArray(raw: string | null | undefined): string[] {
|
|
|
112
168
|
return parsed.filter((v): v is string => typeof v === "string");
|
|
113
169
|
}
|
|
114
170
|
|
|
171
|
+
function isModerationDecision(value: unknown): value is ModerationDecision {
|
|
172
|
+
if (typeof value !== "object" || value === null || !("status" in value)) return false;
|
|
173
|
+
if (value.status !== "pending" && value.status !== "approved" && value.status !== "spam") {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
return !("reason" in value) || value.reason === undefined || typeof value.reason === "string";
|
|
177
|
+
}
|
|
178
|
+
|
|
115
179
|
/** Combined result from a single-pass page contribution collection */
|
|
116
180
|
interface PageContributions {
|
|
117
181
|
metadata: PageMetadataContribution[];
|
|
118
182
|
fragments: PageFragmentContribution[];
|
|
119
183
|
}
|
|
120
184
|
|
|
121
|
-
const VALID_METADATA_KINDS = new Set(["meta", "property", "link", "jsonld"]);
|
|
122
|
-
|
|
123
|
-
/** Security-critical allowlist for link rel values from sandboxed plugins */
|
|
124
|
-
const VALID_LINK_REL = new Set([
|
|
125
|
-
"canonical",
|
|
126
|
-
"alternate",
|
|
127
|
-
"author",
|
|
128
|
-
"license",
|
|
129
|
-
"nlweb",
|
|
130
|
-
"site.standard.document",
|
|
131
|
-
]);
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Runtime validation for sandboxed plugin metadata contributions.
|
|
135
|
-
* Sandboxed plugins return `unknown` across the RPC boundary — we must
|
|
136
|
-
* verify the shape before passing to the metadata collector.
|
|
137
|
-
*/
|
|
138
|
-
function isValidMetadataContribution(c: unknown): c is PageMetadataContribution {
|
|
139
|
-
if (!c || typeof c !== "object" || !("kind" in c)) return false;
|
|
140
|
-
const obj = c as Record<string, unknown>;
|
|
141
|
-
if (typeof obj.kind !== "string" || !VALID_METADATA_KINDS.has(obj.kind)) return false;
|
|
142
|
-
|
|
143
|
-
switch (obj.kind) {
|
|
144
|
-
case "meta":
|
|
145
|
-
return typeof obj.name === "string" && typeof obj.content === "string";
|
|
146
|
-
case "property":
|
|
147
|
-
return typeof obj.property === "string" && typeof obj.content === "string";
|
|
148
|
-
case "link":
|
|
149
|
-
return (
|
|
150
|
-
typeof obj.href === "string" && typeof obj.rel === "string" && VALID_LINK_REL.has(obj.rel)
|
|
151
|
-
);
|
|
152
|
-
case "jsonld":
|
|
153
|
-
return obj.graph != null && typeof obj.graph === "object";
|
|
154
|
-
default:
|
|
155
|
-
return false;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
185
|
import { after } from "./after.js";
|
|
160
186
|
import { maybeRunScheduledBackup } from "./api/handlers/backup.js";
|
|
161
187
|
import { loadBundleFromR2 } from "./api/handlers/marketplace.js";
|
|
@@ -164,6 +190,15 @@ import {
|
|
|
164
190
|
DEFAULT_COMMENT_MODERATOR_PLUGIN_ID,
|
|
165
191
|
defaultCommentModerate,
|
|
166
192
|
} from "./comments/moderator.js";
|
|
193
|
+
import { submitPublicComment } from "./comments/public-submission.js";
|
|
194
|
+
import {
|
|
195
|
+
CommentStatusConflictError,
|
|
196
|
+
createComment,
|
|
197
|
+
moderateComment,
|
|
198
|
+
type CommentCreateInput,
|
|
199
|
+
type CommentCreateResult,
|
|
200
|
+
type CommentHookRunner,
|
|
201
|
+
} from "./comments/service.js";
|
|
167
202
|
import { validateEncryptionKeyAtStartup } from "./config/secrets.js";
|
|
168
203
|
import { OptionsRepository } from "./database/repositories/options.js";
|
|
169
204
|
import {
|
|
@@ -183,6 +218,7 @@ import {
|
|
|
183
218
|
handleContentUnpublish,
|
|
184
219
|
handleContentSchedule,
|
|
185
220
|
handleContentUnschedule,
|
|
221
|
+
handleScheduledPolicyRejection,
|
|
186
222
|
handleContentCountScheduled,
|
|
187
223
|
handleContentDiscardDraft,
|
|
188
224
|
handleContentCompare,
|
|
@@ -208,11 +244,15 @@ import { DEV_CONSOLE_EMAIL_PLUGIN_ID, devConsoleEmailDeliver } from "./plugins/e
|
|
|
208
244
|
import { EmailPipeline } from "./plugins/email.js";
|
|
209
245
|
import {
|
|
210
246
|
createHookPipeline,
|
|
247
|
+
getActiveContentSaveHookName,
|
|
211
248
|
resolveExclusiveHooks as resolveExclusiveHooksShared,
|
|
212
249
|
type HookPipeline,
|
|
213
250
|
} from "./plugins/hooks.js";
|
|
214
|
-
import {
|
|
251
|
+
import { disableRuntimePlugin, enableRuntimePlugin } from "./plugins/lifecycle.js";
|
|
252
|
+
import { HOOK_NAMES, normalizeManifestRoute } from "./plugins/manifest-schema.js";
|
|
253
|
+
import { updatePluginMediaMetadata } from "./plugins/media.js";
|
|
215
254
|
import { extractRequestMeta, sanitizeHeadersForSandbox } from "./plugins/request-meta.js";
|
|
255
|
+
import { PluginRouteRequestError } from "./plugins/route-wire.js";
|
|
216
256
|
import {
|
|
217
257
|
buildRouteMeta,
|
|
218
258
|
parseRouteInput,
|
|
@@ -225,15 +265,41 @@ import { isContentSaveRejection } from "./plugins/save-rejection.js";
|
|
|
225
265
|
import type { CronScheduler } from "./plugins/scheduler/types.js";
|
|
226
266
|
import { PluginStateRepository } from "./plugins/state.js";
|
|
227
267
|
import { syncDeclaredStorageIndexes } from "./plugins/storage-indexes.js";
|
|
228
|
-
import {
|
|
268
|
+
import {
|
|
269
|
+
getRegistryConfigInput,
|
|
270
|
+
resolveManifestRegistryConfig,
|
|
271
|
+
resolveRegistryConfigForSandbox,
|
|
272
|
+
} from "./registry/config.js";
|
|
229
273
|
import { requestCached } from "./request-cache.js";
|
|
230
274
|
import { getRequestContext } from "./request-context.js";
|
|
231
275
|
import { publishDueContent, type PublishedRef } from "./scheduled-publish.js";
|
|
232
276
|
import { FTSManager } from "./search/fts-manager.js";
|
|
233
277
|
import { invalidateSiteSettingsCache } from "./settings/index.js";
|
|
234
278
|
|
|
235
|
-
const DRAFT_ONLY_UPDATE_KEYS = new Set(["data", "slug", "locale", "skipRevision"]);
|
|
279
|
+
const DRAFT_ONLY_UPDATE_KEYS = new Set(["data", "slug", "locale", "skipRevision", "actor"]);
|
|
236
280
|
const MAX_DRAFT_STAGE_ATTEMPTS = 32;
|
|
281
|
+
const PLUGIN_INVOCATION_RELEASE_GRACE_MS = 60_000;
|
|
282
|
+
|
|
283
|
+
type ContentPolicyHookName =
|
|
284
|
+
| "content:beforePublish"
|
|
285
|
+
| "content:beforeSchedule"
|
|
286
|
+
| "content:beforeUnpublish";
|
|
287
|
+
|
|
288
|
+
interface ContentPolicyMutationOptions {
|
|
289
|
+
_rev?: string;
|
|
290
|
+
actor?: ActorInfo;
|
|
291
|
+
origin?: ContentActionOrigin;
|
|
292
|
+
pluginInvocationId?: string;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
type ContentPolicyCheck =
|
|
296
|
+
| { allowed: true; revision: string }
|
|
297
|
+
| {
|
|
298
|
+
allowed: false;
|
|
299
|
+
revision: string;
|
|
300
|
+
error: { code: string; message: string };
|
|
301
|
+
cancellation?: { pluginId: string; reason: string };
|
|
302
|
+
};
|
|
237
303
|
|
|
238
304
|
/**
|
|
239
305
|
* Sandboxed plugin entry from virtual module
|
|
@@ -259,6 +325,10 @@ export interface SandboxedPluginEntry {
|
|
|
259
325
|
adminPages?: Array<{ path: string; label?: string; icon?: string }>;
|
|
260
326
|
/** Dashboard widgets */
|
|
261
327
|
adminWidgets?: Array<{ id: string; title?: string; size?: string }>;
|
|
328
|
+
/** Saved-entry Block Kit panels. */
|
|
329
|
+
editorPanels?: PluginManifest["admin"]["editorPanels"];
|
|
330
|
+
/** Saved-entry host-rendered actions. */
|
|
331
|
+
editorActions?: PluginManifest["admin"]["editorActions"];
|
|
262
332
|
/** Settings schema for the auto-generated admin settings form */
|
|
263
333
|
settingsSchema?: Record<string, SettingField>;
|
|
264
334
|
/** Portable Text block types contributed to the editor (declarative Block Kit) */
|
|
@@ -274,6 +344,26 @@ export interface SandboxedPluginEntry {
|
|
|
274
344
|
preferred?: string[];
|
|
275
345
|
}
|
|
276
346
|
|
|
347
|
+
export type ResolvedPluginEditorExtension =
|
|
348
|
+
| {
|
|
349
|
+
kind: "panel";
|
|
350
|
+
extension: PluginEditorPanel;
|
|
351
|
+
policy: BlockValidationPolicy;
|
|
352
|
+
capabilities: readonly PluginCapability[];
|
|
353
|
+
}
|
|
354
|
+
| {
|
|
355
|
+
kind: "action";
|
|
356
|
+
extension: PluginEditorAction;
|
|
357
|
+
policy: BlockValidationPolicy;
|
|
358
|
+
capabilities: readonly PluginCapability[];
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
export interface PluginEditorExtensionDispatch {
|
|
362
|
+
ui: PluginUiContext;
|
|
363
|
+
kind: "panel" | "action";
|
|
364
|
+
policy: BlockValidationPolicy;
|
|
365
|
+
}
|
|
366
|
+
|
|
277
367
|
/**
|
|
278
368
|
* Media provider entry from virtual module
|
|
279
369
|
*/
|
|
@@ -350,6 +440,15 @@ export interface RuntimeDependencies {
|
|
|
350
440
|
sandboxedPluginEntries: SandboxedPluginEntry[];
|
|
351
441
|
/** Factory function supplied by the active platform adapter. */
|
|
352
442
|
createSandboxRunner: SandboxRunnerFactory | null;
|
|
443
|
+
/** Clock used by cron scheduling, task execution, and scheduled publishing. */
|
|
444
|
+
now?: () => Date;
|
|
445
|
+
/** Site values supplied by hosts that do not load Astro's virtual config. */
|
|
446
|
+
siteInfo?: {
|
|
447
|
+
name?: string;
|
|
448
|
+
url?: string;
|
|
449
|
+
locale?: string;
|
|
450
|
+
trailingSlash?: "always" | "never" | "ignore";
|
|
451
|
+
};
|
|
353
452
|
}
|
|
354
453
|
|
|
355
454
|
/**
|
|
@@ -385,7 +484,16 @@ export interface EmDashRuntimeParts {
|
|
|
385
484
|
db: Kysely<Database>;
|
|
386
485
|
getDb?: () => Kysely<Database>;
|
|
387
486
|
beforeContentWrite?: () => Promise<void>;
|
|
487
|
+
contentCreate?: PluginContentCreateCallback;
|
|
488
|
+
contentActions?: ContentActionCallbacks;
|
|
489
|
+
now?: () => Date;
|
|
388
490
|
storage?: Storage;
|
|
491
|
+
commentModerate?: (
|
|
492
|
+
pluginId: string,
|
|
493
|
+
id: string,
|
|
494
|
+
status: PluginCommentStatus,
|
|
495
|
+
expectedStatus: PluginCommentStatus,
|
|
496
|
+
) => Promise<PluginComment>;
|
|
389
497
|
siteInfo?: {
|
|
390
498
|
siteName?: string;
|
|
391
499
|
siteUrl?: string;
|
|
@@ -404,6 +512,17 @@ export interface EmDashRuntimeParts {
|
|
|
404
512
|
*/
|
|
405
513
|
function beforeSaveFailure(error: unknown) {
|
|
406
514
|
if (isContentSaveRejection(error)) {
|
|
515
|
+
const sandbox = getSandboxSaveRejectionDetails(error);
|
|
516
|
+
if (sandbox) {
|
|
517
|
+
return {
|
|
518
|
+
success: false as const,
|
|
519
|
+
error: {
|
|
520
|
+
code: ErrorCode.SAVE_REJECTED,
|
|
521
|
+
message: error.message,
|
|
522
|
+
details: sandbox,
|
|
523
|
+
},
|
|
524
|
+
};
|
|
525
|
+
}
|
|
407
526
|
return {
|
|
408
527
|
success: false as const,
|
|
409
528
|
error: { code: ErrorCode.SAVE_REJECTED, message: error.message },
|
|
@@ -427,6 +546,27 @@ function contentItemToRecord(item: ContentItemInternal): Record<string, unknown>
|
|
|
427
546
|
return { ...item };
|
|
428
547
|
}
|
|
429
548
|
|
|
549
|
+
function toPluginContentItem(item: ContentItemInternal): PluginContentItem {
|
|
550
|
+
return {
|
|
551
|
+
id: item.id,
|
|
552
|
+
type: item.type,
|
|
553
|
+
slug: item.slug,
|
|
554
|
+
status: item.status,
|
|
555
|
+
locale: item.locale,
|
|
556
|
+
data: item.data,
|
|
557
|
+
createdAt: item.createdAt,
|
|
558
|
+
updatedAt: item.updatedAt,
|
|
559
|
+
publishedAt: item.publishedAt,
|
|
560
|
+
scheduledAt: item.scheduledAt,
|
|
561
|
+
authorId: item.authorId,
|
|
562
|
+
translationGroup: item.translationGroup,
|
|
563
|
+
liveRevisionId: item.liveRevisionId,
|
|
564
|
+
draftRevisionId: item.draftRevisionId,
|
|
565
|
+
version: item.version,
|
|
566
|
+
...(item.seo === undefined ? {} : { seo: item.seo }),
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
430
570
|
/**
|
|
431
571
|
* Db init lock reclaim deadline. Derived from the migration race wait so
|
|
432
572
|
* they can't drift apart: a healthy init can legitimately block for the
|
|
@@ -507,6 +647,7 @@ function getSeedHolder(): SeedHolder {
|
|
|
507
647
|
}
|
|
508
648
|
const storageCache = new Map<string, Storage>();
|
|
509
649
|
const sandboxedPluginCache = new Map<string, SandboxedPluginInstance>();
|
|
650
|
+
const sandboxedManifestCache = new Map<string, PluginManifest>();
|
|
510
651
|
/**
|
|
511
652
|
* Per-tier sets of `${pluginId}:${version}` keys present in
|
|
512
653
|
* `sandboxedPluginCache`. Used during sync to know which entries belong
|
|
@@ -529,15 +670,35 @@ const marketplaceManifestCache = new Map<
|
|
|
529
670
|
pages?: PluginAdminPage[];
|
|
530
671
|
widgets?: PluginDashboardWidget[];
|
|
531
672
|
settingsSchema?: Record<string, SettingField>;
|
|
673
|
+
editorPanels?: PluginManifest["admin"]["editorPanels"];
|
|
674
|
+
editorActions?: PluginManifest["admin"]["editorActions"];
|
|
532
675
|
};
|
|
533
676
|
mcp?: PluginMcpManifestConfig;
|
|
534
677
|
storage?: PluginManifest["storage"];
|
|
678
|
+
allowedHosts?: string[];
|
|
679
|
+
capabilities?: PluginCapability[];
|
|
535
680
|
}
|
|
536
681
|
>();
|
|
537
682
|
/** Route metadata for sandboxed plugins: pluginId -> routeName -> RouteMeta */
|
|
538
683
|
const sandboxedRouteMetaCache = new Map<string, Map<string, RouteMeta>>();
|
|
539
684
|
let sandboxRunner: SandboxRunner | null = null;
|
|
540
685
|
|
|
686
|
+
function allowedBrowserImageHosts(
|
|
687
|
+
capabilities: readonly PluginCapability[],
|
|
688
|
+
allowedHosts: readonly string[],
|
|
689
|
+
): readonly string[] {
|
|
690
|
+
if (
|
|
691
|
+
capabilities.includes("network:request:unrestricted") ||
|
|
692
|
+
capabilities.includes("network:fetch:any")
|
|
693
|
+
) {
|
|
694
|
+
return ["*"];
|
|
695
|
+
}
|
|
696
|
+
if (capabilities.includes("network:request") || capabilities.includes("network:fetch")) {
|
|
697
|
+
return allowedHosts;
|
|
698
|
+
}
|
|
699
|
+
return [];
|
|
700
|
+
}
|
|
701
|
+
|
|
541
702
|
/**
|
|
542
703
|
* EmDashRuntime - singleton per worker
|
|
543
704
|
*/
|
|
@@ -576,6 +737,16 @@ export class EmDashRuntime {
|
|
|
576
737
|
private cronScheduler: CronScheduler | null;
|
|
577
738
|
private enabledPlugins: Set<string>;
|
|
578
739
|
private pluginStates: Map<string, string>;
|
|
740
|
+
private readonly activePluginContentActions = new Map<string, number>();
|
|
741
|
+
private readonly pendingPluginAfterHooks = new Map<string, Array<() => Promise<void>>>();
|
|
742
|
+
private readonly activePluginInvocations = new Set<string>();
|
|
743
|
+
/** Timed-out sandbox invocations whose later actions must schedule hooks without another flush. */
|
|
744
|
+
private readonly releasedPluginInvocations = new Set<string>();
|
|
745
|
+
private readonly pluginInvocationCacheInvalidators = new Map<
|
|
746
|
+
string,
|
|
747
|
+
PluginContentCacheInvalidator
|
|
748
|
+
>();
|
|
749
|
+
private pluginContentCacheInvalidator?: PluginContentCacheInvalidator;
|
|
579
750
|
|
|
580
751
|
/**
|
|
581
752
|
* Isolate-lifetime guard so FTS indexes are verified at most once per
|
|
@@ -600,7 +771,16 @@ export class EmDashRuntime {
|
|
|
600
771
|
db: Kysely<Database>;
|
|
601
772
|
getDb?: () => Kysely<Database>;
|
|
602
773
|
beforeContentWrite?: () => Promise<void>;
|
|
774
|
+
contentCreate?: PluginContentCreateCallback;
|
|
775
|
+
contentActions?: ContentActionCallbacks;
|
|
776
|
+
now?: () => Date;
|
|
603
777
|
storage?: Storage;
|
|
778
|
+
commentModerate?: (
|
|
779
|
+
pluginId: string,
|
|
780
|
+
id: string,
|
|
781
|
+
status: PluginCommentStatus,
|
|
782
|
+
expectedStatus: PluginCommentStatus,
|
|
783
|
+
) => Promise<PluginComment>;
|
|
604
784
|
siteInfo?: {
|
|
605
785
|
siteName?: string;
|
|
606
786
|
siteUrl?: string;
|
|
@@ -612,6 +792,7 @@ export class EmDashRuntime {
|
|
|
612
792
|
private runtimeDeps: RuntimeDependencies;
|
|
613
793
|
/** Mutable ref for the cron invokeCronHook closure to read the current pipeline */
|
|
614
794
|
private pipelineRef!: { current: HookPipeline };
|
|
795
|
+
private readonly commentModerationInProgress = new Set<string>();
|
|
615
796
|
|
|
616
797
|
/**
|
|
617
798
|
* Get the database instance for the current request.
|
|
@@ -629,6 +810,13 @@ export class EmDashRuntime {
|
|
|
629
810
|
return this._db;
|
|
630
811
|
}
|
|
631
812
|
|
|
813
|
+
/** Bind the platform cache provider used by plugin actions, including request-less cron hooks. */
|
|
814
|
+
setPluginContentCacheInvalidator(
|
|
815
|
+
invalidateContentCache: PluginContentCacheInvalidator | undefined,
|
|
816
|
+
): void {
|
|
817
|
+
this.pluginContentCacheInvalidator = invalidateContentCache;
|
|
818
|
+
}
|
|
819
|
+
|
|
632
820
|
constructor(parts: EmDashRuntimeParts) {
|
|
633
821
|
this._db = parts.db;
|
|
634
822
|
this.storage = parts.storage;
|
|
@@ -679,9 +867,15 @@ export class EmDashRuntime {
|
|
|
679
867
|
onPublished?: (refs: PublishedRef[]) => Promise<void>,
|
|
680
868
|
): Promise<PublishedRef[]> {
|
|
681
869
|
await assertMediaUsageActivationWriteAllowed(this.db);
|
|
870
|
+
const currentTime = this.runtimeDeps.now?.() ?? new Date();
|
|
682
871
|
return publishDueContent(this.db, {
|
|
683
|
-
publish: (collection, id, options) =>
|
|
872
|
+
publish: (collection, id, options) =>
|
|
873
|
+
this.handleContentPublish(collection, id, {
|
|
874
|
+
...options,
|
|
875
|
+
origin: { source: "scheduler" },
|
|
876
|
+
}),
|
|
684
877
|
onPublished,
|
|
878
|
+
currentTime,
|
|
685
879
|
});
|
|
686
880
|
}
|
|
687
881
|
|
|
@@ -702,11 +896,26 @@ export class EmDashRuntime {
|
|
|
702
896
|
async runScheduledTasks(
|
|
703
897
|
options: {
|
|
704
898
|
onPublished?: (refs: PublishedRef[]) => Promise<void>;
|
|
899
|
+
invalidateContentCache?: PluginContentCacheInvalidator;
|
|
705
900
|
} = {},
|
|
706
901
|
): Promise<{ published: PublishedRef[] }> {
|
|
902
|
+
const { published } = await this.runScheduledTasksWithStats(options);
|
|
903
|
+
return { published };
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
async runScheduledTasksWithStats(
|
|
907
|
+
options: {
|
|
908
|
+
onPublished?: (refs: PublishedRef[]) => Promise<void>;
|
|
909
|
+
invalidateContentCache?: PluginContentCacheInvalidator;
|
|
910
|
+
} = {},
|
|
911
|
+
): Promise<{ processed: number; published: PublishedRef[] }> {
|
|
912
|
+
if (options.invalidateContentCache) {
|
|
913
|
+
this.pluginContentCacheInvalidator = options.invalidateContentCache;
|
|
914
|
+
}
|
|
915
|
+
let processed = 0;
|
|
707
916
|
if (this.cronExecutor) {
|
|
708
917
|
try {
|
|
709
|
-
await this.cronExecutor.tick();
|
|
918
|
+
processed = await this.cronExecutor.tick();
|
|
710
919
|
} catch (error) {
|
|
711
920
|
console.error("[cron] Tick failed:", error);
|
|
712
921
|
}
|
|
@@ -740,7 +949,7 @@ export class EmDashRuntime {
|
|
|
740
949
|
await maybeRunScheduledBackup(this.db, this.storage ?? undefined);
|
|
741
950
|
await recordSchedulerHeartbeatSafely(this.db);
|
|
742
951
|
|
|
743
|
-
return { published };
|
|
952
|
+
return { processed, published };
|
|
744
953
|
}
|
|
745
954
|
|
|
746
955
|
/**
|
|
@@ -771,6 +980,20 @@ export class EmDashRuntime {
|
|
|
771
980
|
}
|
|
772
981
|
}
|
|
773
982
|
|
|
983
|
+
/** Stop background work and discard loaded sandbox isolates. */
|
|
984
|
+
async shutdown(): Promise<void> {
|
|
985
|
+
await this.stopCron();
|
|
986
|
+
sandboxRunner?.setCommentModerate?.(null);
|
|
987
|
+
await sandboxRunner?.terminateAll();
|
|
988
|
+
sandboxRunner = null;
|
|
989
|
+
sandboxedPluginCache.clear();
|
|
990
|
+
sandboxedManifestCache.clear();
|
|
991
|
+
sandboxedRouteMetaCache.clear();
|
|
992
|
+
marketplaceManifestCache.clear();
|
|
993
|
+
marketplacePluginKeys.clear();
|
|
994
|
+
registryPluginKeys.clear();
|
|
995
|
+
}
|
|
996
|
+
|
|
774
997
|
/**
|
|
775
998
|
* Update in-memory plugin status and rebuild the hook pipeline.
|
|
776
999
|
*
|
|
@@ -792,6 +1015,43 @@ export class EmDashRuntime {
|
|
|
792
1015
|
}
|
|
793
1016
|
}
|
|
794
1017
|
|
|
1018
|
+
async handlePluginEnable(pluginId: string) {
|
|
1019
|
+
return enableRuntimePlugin(this, pluginId);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
async handlePluginDisable(pluginId: string) {
|
|
1023
|
+
return disableRuntimePlugin(this, pluginId);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/** Dispatch teardown lifecycle while the runtime-installed isolate is still loaded. */
|
|
1027
|
+
async runPluginUninstallLifecycle(pluginId: string, deleteData: boolean): Promise<void> {
|
|
1028
|
+
const active = this.isPluginEnabled(pluginId);
|
|
1029
|
+
if (active) {
|
|
1030
|
+
try {
|
|
1031
|
+
await this._hooks.runPluginDeactivate(pluginId);
|
|
1032
|
+
} catch (error) {
|
|
1033
|
+
console.error(`EmDash: Plugin ${pluginId} deactivate hook failed during uninstall:`, error);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
try {
|
|
1038
|
+
const results = await this._hooks.runPluginUninstall(pluginId, deleteData);
|
|
1039
|
+
if (results.length > 0) return;
|
|
1040
|
+
await this.findSandboxedPlugin(pluginId)?.invokeHook("plugin:uninstall", { deleteData });
|
|
1041
|
+
} catch (error) {
|
|
1042
|
+
console.error(`EmDash: Plugin ${pluginId} uninstall hook failed:`, error);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
async runPluginInstallLifecycle(pluginId: string): Promise<void> {
|
|
1047
|
+
await this._hooks.runPluginInstall(pluginId);
|
|
1048
|
+
await this._hooks.runPluginActivate(pluginId);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
async runPluginActivateLifecycle(pluginId: string): Promise<void> {
|
|
1052
|
+
await this._hooks.runPluginActivate(pluginId);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
795
1055
|
/**
|
|
796
1056
|
* Rebuild the hook pipeline from the current set of enabled plugins.
|
|
797
1057
|
*
|
|
@@ -864,7 +1124,7 @@ export class EmDashRuntime {
|
|
|
864
1124
|
* update, and uninstall handlers complete.
|
|
865
1125
|
*/
|
|
866
1126
|
async syncRegistryPlugins(): Promise<void> {
|
|
867
|
-
if (!this.config.experimental?.registry) return;
|
|
1127
|
+
if (!getRegistryConfigInput(this.config.registry, this.config.experimental?.registry)) return;
|
|
868
1128
|
await this.syncSandboxedSourcePlugins("registry");
|
|
869
1129
|
}
|
|
870
1130
|
|
|
@@ -882,6 +1142,7 @@ export class EmDashRuntime {
|
|
|
882
1142
|
if (!sandboxRunner || !sandboxRunner.isAvailable()) return;
|
|
883
1143
|
|
|
884
1144
|
const keySet = source === "marketplace" ? marketplacePluginKeys : registryPluginKeys;
|
|
1145
|
+
let pipelineChanged = false;
|
|
885
1146
|
|
|
886
1147
|
try {
|
|
887
1148
|
const stateRepo = new PluginStateRepository(this.db);
|
|
@@ -935,8 +1196,11 @@ export class EmDashRuntime {
|
|
|
935
1196
|
}
|
|
936
1197
|
|
|
937
1198
|
sandboxedPluginCache.delete(key);
|
|
1199
|
+
sandboxedManifestCache.delete(key);
|
|
938
1200
|
this.sandboxedPlugins.delete(key);
|
|
1201
|
+
this.removePluginFromLists(pluginId);
|
|
939
1202
|
keySet.delete(key);
|
|
1203
|
+
pipelineChanged = true;
|
|
940
1204
|
if (pluginId) {
|
|
941
1205
|
sandboxedRouteMetaCache.delete(pluginId);
|
|
942
1206
|
marketplaceManifestCache.delete(pluginId);
|
|
@@ -959,8 +1223,11 @@ export class EmDashRuntime {
|
|
|
959
1223
|
|
|
960
1224
|
const loaded = await sandboxRunner.load(bundle.manifest, bundle.backendCode);
|
|
961
1225
|
sandboxedPluginCache.set(key, loaded);
|
|
1226
|
+
sandboxedManifestCache.set(key, bundle.manifest);
|
|
962
1227
|
this.sandboxedPlugins.set(key, loaded);
|
|
1228
|
+
this.allPipelinePlugins.push(createSandboxedPluginProxy(bundle.manifest, loaded));
|
|
963
1229
|
keySet.add(key);
|
|
1230
|
+
pipelineChanged = true;
|
|
964
1231
|
|
|
965
1232
|
// Cache manifest admin config for getManifest()
|
|
966
1233
|
marketplaceManifestCache.set(pluginId, {
|
|
@@ -969,6 +1236,8 @@ export class EmDashRuntime {
|
|
|
969
1236
|
admin: bundle.manifest.admin,
|
|
970
1237
|
mcp: bundle.manifest.mcp,
|
|
971
1238
|
storage: bundle.manifest.storage,
|
|
1239
|
+
allowedHosts: bundle.manifest.allowedHosts,
|
|
1240
|
+
capabilities: bundle.manifest.capabilities,
|
|
972
1241
|
});
|
|
973
1242
|
|
|
974
1243
|
// Cache route metadata from manifest for auth decisions
|
|
@@ -983,6 +1252,10 @@ export class EmDashRuntime {
|
|
|
983
1252
|
sandboxedRouteMetaCache.delete(pluginId);
|
|
984
1253
|
}
|
|
985
1254
|
}
|
|
1255
|
+
|
|
1256
|
+
if (pipelineChanged) {
|
|
1257
|
+
await this.rebuildHookPipeline();
|
|
1258
|
+
}
|
|
986
1259
|
} catch (error) {
|
|
987
1260
|
console.error(`EmDash: Failed to sync ${source} plugins:`, error);
|
|
988
1261
|
}
|
|
@@ -1085,6 +1358,8 @@ export class EmDashRuntime {
|
|
|
1085
1358
|
admin: bundle.manifest.admin,
|
|
1086
1359
|
mcp: bundle.manifest.mcp,
|
|
1087
1360
|
storage: bundle.manifest.storage,
|
|
1361
|
+
allowedHosts: bundle.manifest.allowedHosts,
|
|
1362
|
+
capabilities: bundle.manifest.capabilities,
|
|
1088
1363
|
});
|
|
1089
1364
|
if (bundle.manifest.routes.length > 0) {
|
|
1090
1365
|
const routeMetaMap = new Map<string, RouteMeta>();
|
|
@@ -1135,6 +1410,8 @@ export class EmDashRuntime {
|
|
|
1135
1410
|
w.size === "full" || w.size === "half" || w.size === "third" ? w.size : undefined,
|
|
1136
1411
|
})),
|
|
1137
1412
|
settingsSchema: bundle.manifest.admin?.settingsSchema,
|
|
1413
|
+
editorPanels: bundle.manifest.admin?.editorPanels,
|
|
1414
|
+
editorActions: bundle.manifest.admin?.editorActions,
|
|
1138
1415
|
});
|
|
1139
1416
|
newPlugins.push(adapted);
|
|
1140
1417
|
this.allPipelinePlugins.push(adapted);
|
|
@@ -1201,10 +1478,9 @@ export class EmDashRuntime {
|
|
|
1201
1478
|
};
|
|
1202
1479
|
|
|
1203
1480
|
// Validate EMDASH_ENCRYPTION_KEY once here so a malformed value
|
|
1204
|
-
// surfaces in startup logs
|
|
1205
|
-
//
|
|
1206
|
-
//
|
|
1207
|
-
// misconfiguration.
|
|
1481
|
+
// surfaces in startup logs. Plugin secret-setting operations re-read
|
|
1482
|
+
// the key and fail closed at their own boundary, so validation here
|
|
1483
|
+
// does not block unrelated request paths.
|
|
1208
1484
|
await phase("rt.secrets", "Validate encryption key", () => validateEncryptionKeyAtStartup());
|
|
1209
1485
|
|
|
1210
1486
|
// FTS verify/repair is deferred off the cold-start hot path.
|
|
@@ -1283,13 +1559,13 @@ export class EmDashRuntime {
|
|
|
1283
1559
|
]);
|
|
1284
1560
|
storedLocaleCasingRepairVersion = siteOpts.get(LOCALE_CASING_REPAIR_OPTION);
|
|
1285
1561
|
return {
|
|
1286
|
-
siteName: siteOpts.get("emdash:site_title") ?? undefined,
|
|
1287
|
-
siteUrl: siteOpts.get("emdash:site_url") ?? undefined,
|
|
1288
|
-
locale: siteOpts.get("emdash:locale") ?? undefined,
|
|
1562
|
+
siteName: deps.siteInfo?.name ?? siteOpts.get("emdash:site_title") ?? undefined,
|
|
1563
|
+
siteUrl: deps.siteInfo?.url ?? siteOpts.get("emdash:site_url") ?? undefined,
|
|
1564
|
+
locale: deps.siteInfo?.locale ?? siteOpts.get("emdash:locale") ?? undefined,
|
|
1289
1565
|
// trailingSlash is a build-time Astro routing decision, not a
|
|
1290
1566
|
// user-editable setting, so it comes from the Astro config
|
|
1291
1567
|
// (virtual:emdash/config), not the options table.
|
|
1292
|
-
trailingSlash: virtualConfig?.trailingSlash,
|
|
1568
|
+
trailingSlash: deps.siteInfo?.trailingSlash ?? virtualConfig?.trailingSlash,
|
|
1293
1569
|
};
|
|
1294
1570
|
};
|
|
1295
1571
|
|
|
@@ -1320,17 +1596,18 @@ export class EmDashRuntime {
|
|
|
1320
1596
|
coldStartReads.push(
|
|
1321
1597
|
phase("rt.seedcheck", "Auto-seed gate", async () => {
|
|
1322
1598
|
try {
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1599
|
+
// Selecting the slugs instead of COUNT(*) costs the same
|
|
1600
|
+
// round trip and primes the registered-collections cache,
|
|
1601
|
+
// so the first render on this isolate skips its own lookup.
|
|
1602
|
+
const [collectionRows, setupOption] = await Promise.all([
|
|
1603
|
+
readDb.selectFrom("_emdash_collections").select("slug").execute(),
|
|
1328
1604
|
readDb
|
|
1329
1605
|
.selectFrom("options")
|
|
1330
1606
|
.select("value")
|
|
1331
1607
|
.where("name", "=", "emdash:setup_complete")
|
|
1332
1608
|
.executeTakeFirst(),
|
|
1333
1609
|
]);
|
|
1610
|
+
primeRegisteredCollections(collectionRows.map((row) => row.slug));
|
|
1334
1611
|
const setupDone = (() => {
|
|
1335
1612
|
try {
|
|
1336
1613
|
return !!setupOption && JSON.parse(setupOption.value) === true;
|
|
@@ -1338,7 +1615,7 @@ export class EmDashRuntime {
|
|
|
1338
1615
|
return false;
|
|
1339
1616
|
}
|
|
1340
1617
|
})();
|
|
1341
|
-
seedGate = { collectionCount:
|
|
1618
|
+
seedGate = { collectionCount: collectionRows.length, setupDone };
|
|
1342
1619
|
} catch (error) {
|
|
1343
1620
|
captureMissingManualSchema(error);
|
|
1344
1621
|
// Leave the "already set up" default so a read failure never
|
|
@@ -1532,26 +1809,8 @@ export class EmDashRuntime {
|
|
|
1532
1809
|
}
|
|
1533
1810
|
}
|
|
1534
1811
|
|
|
1535
|
-
// Filter to currently enabled plugins for the initial pipeline
|
|
1536
|
-
const enabledPluginList = allPipelinePlugins.filter((p) => enabledPlugins.has(p.id));
|
|
1537
|
-
|
|
1538
|
-
// Create hook pipeline. getDb travels here (not just via the email
|
|
1539
|
-
// setContextFactory call below) so it survives rebuildHookPipeline(),
|
|
1540
|
-
// which reconstructs the factory from pipelineFactoryOptions. Without it,
|
|
1541
|
-
// toggling a plugin on an email-less deployment would silently revert
|
|
1542
|
-
// plugin contexts to the singleton db — re-breaking connection-backed
|
|
1543
|
-
// adapters. See #1622.
|
|
1544
|
-
const pipelineFactoryOptions = {
|
|
1545
|
-
db,
|
|
1546
|
-
getDb: resolveDb,
|
|
1547
|
-
beforeContentWrite: () => assertMediaUsageActivationWriteAllowed(resolveDb()),
|
|
1548
|
-
storage: storage ?? undefined,
|
|
1549
|
-
siteInfo,
|
|
1550
|
-
};
|
|
1551
|
-
const pipeline = createHookPipeline(enabledPluginList, pipelineFactoryOptions);
|
|
1552
|
-
|
|
1553
1812
|
// Load sandboxed plugins (build-time, sandbox runner path)
|
|
1554
|
-
const
|
|
1813
|
+
const sandboxedPluginPool = await phase("rt.sandbox", "Sandboxed plugins", () =>
|
|
1555
1814
|
EmDashRuntime.loadSandboxedPlugins(deps, db, storage, siteInfo),
|
|
1556
1815
|
);
|
|
1557
1816
|
|
|
@@ -1569,7 +1828,7 @@ export class EmDashRuntime {
|
|
|
1569
1828
|
db,
|
|
1570
1829
|
storage,
|
|
1571
1830
|
deps,
|
|
1572
|
-
|
|
1831
|
+
sandboxedPluginPool,
|
|
1573
1832
|
siteInfo,
|
|
1574
1833
|
),
|
|
1575
1834
|
),
|
|
@@ -1577,7 +1836,10 @@ export class EmDashRuntime {
|
|
|
1577
1836
|
}
|
|
1578
1837
|
|
|
1579
1838
|
// Cold-start: load registry-installed plugins from site R2
|
|
1580
|
-
if (
|
|
1839
|
+
if (
|
|
1840
|
+
getRegistryConfigInput(deps.config.registry, deps.config.experimental?.registry) &&
|
|
1841
|
+
storage
|
|
1842
|
+
) {
|
|
1581
1843
|
installedTierPhases.push(
|
|
1582
1844
|
phase("rt.registry", "Registry plugins", () =>
|
|
1583
1845
|
EmDashRuntime.loadInstalledSandboxedPlugins(
|
|
@@ -1585,7 +1847,7 @@ export class EmDashRuntime {
|
|
|
1585
1847
|
db,
|
|
1586
1848
|
storage,
|
|
1587
1849
|
deps,
|
|
1588
|
-
|
|
1850
|
+
sandboxedPluginPool,
|
|
1589
1851
|
siteInfo,
|
|
1590
1852
|
),
|
|
1591
1853
|
),
|
|
@@ -1595,6 +1857,113 @@ export class EmDashRuntime {
|
|
|
1595
1857
|
await Promise.all(installedTierPhases);
|
|
1596
1858
|
}
|
|
1597
1859
|
|
|
1860
|
+
const configuredSandboxIds = new Set(deps.sandboxedPluginEntries.map((entry) => entry.id));
|
|
1861
|
+
const sandboxedPlugins = new Map(
|
|
1862
|
+
[...sandboxedPluginPool].filter(([pluginKey]) => {
|
|
1863
|
+
const pluginId = pluginKey.slice(0, pluginKey.lastIndexOf(":"));
|
|
1864
|
+
return configuredSandboxIds.has(pluginId) || pluginStates.get(pluginId) === "active";
|
|
1865
|
+
}),
|
|
1866
|
+
);
|
|
1867
|
+
|
|
1868
|
+
// Add isolate-backed proxies to the same host pipeline as trusted
|
|
1869
|
+
// plugins. The proxy carries only manifest metadata and RPC handlers;
|
|
1870
|
+
// plugin code remains inside the configured runner.
|
|
1871
|
+
for (const [pluginKey, instance] of sandboxedPlugins) {
|
|
1872
|
+
const manifest = sandboxedManifestCache.get(pluginKey);
|
|
1873
|
+
if (!manifest) continue;
|
|
1874
|
+
allPipelinePlugins.push(createSandboxedPluginProxy(manifest, instance));
|
|
1875
|
+
const status = pluginStates.get(manifest.id);
|
|
1876
|
+
if (status === undefined || status === "active") enabledPlugins.add(manifest.id);
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
// Create hook pipeline. getDb travels here (not just via the email
|
|
1880
|
+
// setContextFactory call below) so it survives rebuildHookPipeline(),
|
|
1881
|
+
// which reconstructs the factory from pipelineFactoryOptions. Without it,
|
|
1882
|
+
// toggling a plugin on an email-less deployment would silently revert
|
|
1883
|
+
// plugin contexts to the singleton db — re-breaking connection-backed
|
|
1884
|
+
// adapters. See #1622.
|
|
1885
|
+
const runtimeRef: { current: EmDashRuntime | null } = { current: null };
|
|
1886
|
+
const requireRuntime = (): EmDashRuntime => {
|
|
1887
|
+
if (!runtimeRef.current) throw new Error("EmDash runtime is not ready");
|
|
1888
|
+
return runtimeRef.current;
|
|
1889
|
+
};
|
|
1890
|
+
const contentActions: ContentActionCallbacks = {
|
|
1891
|
+
begin: (pluginId, invocationId, invalidateContentCache) =>
|
|
1892
|
+
requireRuntime().beginPluginInvocation(pluginId, invocationId, invalidateContentCache),
|
|
1893
|
+
flush: (pluginId, invocationId, final) =>
|
|
1894
|
+
requireRuntime().flushPluginAfterHooks(pluginId, invocationId, final),
|
|
1895
|
+
getVersioned: (pluginId, collection, id) =>
|
|
1896
|
+
requireRuntime().pluginGetVersioned(pluginId, collection, id),
|
|
1897
|
+
publish: (pluginId, collection, id, options, invocationId, invalidateContentCache) =>
|
|
1898
|
+
requireRuntime().pluginPublish(
|
|
1899
|
+
pluginId,
|
|
1900
|
+
collection,
|
|
1901
|
+
id,
|
|
1902
|
+
options,
|
|
1903
|
+
invocationId,
|
|
1904
|
+
invalidateContentCache,
|
|
1905
|
+
),
|
|
1906
|
+
unpublish: (pluginId, collection, id, options, invocationId, invalidateContentCache) =>
|
|
1907
|
+
requireRuntime().pluginUnpublish(
|
|
1908
|
+
pluginId,
|
|
1909
|
+
collection,
|
|
1910
|
+
id,
|
|
1911
|
+
options,
|
|
1912
|
+
invocationId,
|
|
1913
|
+
invalidateContentCache,
|
|
1914
|
+
),
|
|
1915
|
+
schedule: (pluginId, collection, id, options, invocationId, invalidateContentCache) =>
|
|
1916
|
+
requireRuntime().pluginSchedule(
|
|
1917
|
+
pluginId,
|
|
1918
|
+
collection,
|
|
1919
|
+
id,
|
|
1920
|
+
options,
|
|
1921
|
+
invocationId,
|
|
1922
|
+
invalidateContentCache,
|
|
1923
|
+
),
|
|
1924
|
+
unschedule: (pluginId, collection, id, options, invocationId, invalidateContentCache) =>
|
|
1925
|
+
requireRuntime().pluginUnschedule(
|
|
1926
|
+
pluginId,
|
|
1927
|
+
collection,
|
|
1928
|
+
id,
|
|
1929
|
+
options,
|
|
1930
|
+
invocationId,
|
|
1931
|
+
invalidateContentCache,
|
|
1932
|
+
),
|
|
1933
|
+
getTrashedVersioned: (pluginId, collection, id) =>
|
|
1934
|
+
requireRuntime().pluginGetTrashedVersioned(pluginId, collection, id),
|
|
1935
|
+
restore: (pluginId, collection, id, options, invocationId, invalidateContentCache) =>
|
|
1936
|
+
requireRuntime().pluginRestore(
|
|
1937
|
+
pluginId,
|
|
1938
|
+
collection,
|
|
1939
|
+
id,
|
|
1940
|
+
options,
|
|
1941
|
+
invocationId,
|
|
1942
|
+
invalidateContentCache,
|
|
1943
|
+
),
|
|
1944
|
+
};
|
|
1945
|
+
const pipelineFactoryOptions = {
|
|
1946
|
+
db,
|
|
1947
|
+
getDb: resolveDb,
|
|
1948
|
+
beforeContentWrite: () => assertMediaUsageActivationWriteAllowed(resolveDb()),
|
|
1949
|
+
contentActions,
|
|
1950
|
+
now: deps.now,
|
|
1951
|
+
storage: storage ?? undefined,
|
|
1952
|
+
siteInfo,
|
|
1953
|
+
commentModerate: (
|
|
1954
|
+
pluginId: string,
|
|
1955
|
+
id: string,
|
|
1956
|
+
status: PluginCommentStatus,
|
|
1957
|
+
expectedStatus: PluginCommentStatus,
|
|
1958
|
+
) => {
|
|
1959
|
+
const current = runtimeRef.current;
|
|
1960
|
+
if (!current) throw new Error("Comment moderation is unavailable during runtime startup");
|
|
1961
|
+
return current.handlePluginCommentModerate(pluginId, id, status, expectedStatus);
|
|
1962
|
+
},
|
|
1963
|
+
};
|
|
1964
|
+
const enabledPluginList = allPipelinePlugins.filter((p) => enabledPlugins.has(p.id));
|
|
1965
|
+
const pipeline = createHookPipeline(enabledPluginList, pipelineFactoryOptions);
|
|
1966
|
+
|
|
1598
1967
|
// Initialize media providers
|
|
1599
1968
|
const mediaProviders = new Map<string, MediaProvider>();
|
|
1600
1969
|
const mediaProviderEntries = deps.mediaProviderEntries ?? [];
|
|
@@ -1650,11 +2019,9 @@ export class EmDashRuntime {
|
|
|
1650
2019
|
// so the timer scheduler's cleanup can route scheduled publishing through
|
|
1651
2020
|
// the runtime wrapper (firing content:afterPublish hooks). The first tick
|
|
1652
2021
|
// is ≥1s out, well after the synchronous assignment below.
|
|
1653
|
-
const runtimeRef: { current: EmDashRuntime | null } = { current: null };
|
|
1654
|
-
|
|
1655
2022
|
await phase("rt.cron", "Cron init (recovery deferred post-response)", async () => {
|
|
1656
2023
|
try {
|
|
1657
|
-
cronExecutor = new CronExecutor(resolveDb, invokeCronHook);
|
|
2024
|
+
cronExecutor = new CronExecutor(resolveDb, invokeCronHook, deps.now);
|
|
1658
2025
|
// Plugin schedules are always database-backed. On long-lived runtimes this
|
|
1659
2026
|
// callback also wakes the timer; on Cloudflare the external Cron Trigger
|
|
1660
2027
|
// drives execution, so rescheduling is intentionally a no-op.
|
|
@@ -1733,6 +2100,7 @@ export class EmDashRuntime {
|
|
|
1733
2100
|
// Non-fatal — CMS works without cron
|
|
1734
2101
|
}
|
|
1735
2102
|
});
|
|
2103
|
+
sandboxRunner?.setCronReschedule?.(() => cronScheduler?.reschedule());
|
|
1736
2104
|
|
|
1737
2105
|
const runtime = new EmDashRuntime({
|
|
1738
2106
|
db,
|
|
@@ -1757,9 +2125,46 @@ export class EmDashRuntime {
|
|
|
1757
2125
|
runtimeDeps: deps,
|
|
1758
2126
|
pipelineRef,
|
|
1759
2127
|
});
|
|
2128
|
+
const contentCreate: PluginContentCreateCallback = async (
|
|
2129
|
+
_pluginId,
|
|
2130
|
+
collection,
|
|
2131
|
+
input,
|
|
2132
|
+
options,
|
|
2133
|
+
) => {
|
|
2134
|
+
const { seo, ...data } = input;
|
|
2135
|
+
const result = await runtime.handleContentCreate(
|
|
2136
|
+
collection,
|
|
2137
|
+
{
|
|
2138
|
+
data,
|
|
2139
|
+
seo,
|
|
2140
|
+
locale: resolveContentCreateLocale(options?.locale),
|
|
2141
|
+
translationOf: options?.translationOf,
|
|
2142
|
+
},
|
|
2143
|
+
{
|
|
2144
|
+
skipSaveHooks: options?.sandboxOrigin
|
|
2145
|
+
? options.originHook !== undefined
|
|
2146
|
+
: getActiveContentSaveHookName() != null,
|
|
2147
|
+
excludeAfterSavePluginId: _pluginId,
|
|
2148
|
+
},
|
|
2149
|
+
);
|
|
2150
|
+
if (!result.success) {
|
|
2151
|
+
throw Object.assign(new Error(result.error.message), {
|
|
2152
|
+
name: result.error.code,
|
|
2153
|
+
code: result.error.code,
|
|
2154
|
+
});
|
|
2155
|
+
}
|
|
2156
|
+
return result.data.item;
|
|
2157
|
+
};
|
|
2158
|
+
runtime.pipelineFactoryOptions.contentCreate = contentCreate;
|
|
2159
|
+
pipeline.setContextFactory({ contentCreate });
|
|
2160
|
+
sandboxRunner?.setContentCreate?.(contentCreate);
|
|
1760
2161
|
// Hand the constructed instance to the scheduler-cleanup closure so the
|
|
1761
2162
|
// timer-driven sweep can fire publish hooks (see runtimeRef above).
|
|
1762
2163
|
runtimeRef.current = runtime;
|
|
2164
|
+
sandboxRunner?.setCommentModerate?.((pluginId, id, status, expectedStatus) =>
|
|
2165
|
+
runtime.handlePluginCommentModerate(pluginId, id, status, expectedStatus),
|
|
2166
|
+
);
|
|
2167
|
+
sandboxRunner?.setContentActions?.(contentActions);
|
|
1763
2168
|
return runtime;
|
|
1764
2169
|
}
|
|
1765
2170
|
|
|
@@ -1987,6 +2392,8 @@ export class EmDashRuntime {
|
|
|
1987
2392
|
settingsSchema: entry.settingsSchema,
|
|
1988
2393
|
portableTextBlocks: entry.portableTextBlocks,
|
|
1989
2394
|
fieldWidgets: entry.fieldWidgets,
|
|
2395
|
+
editorPanels: entry.editorPanels,
|
|
2396
|
+
editorActions: entry.editorActions,
|
|
1990
2397
|
});
|
|
1991
2398
|
plugins.push(resolved);
|
|
1992
2399
|
console.log(
|
|
@@ -2013,11 +2420,6 @@ export class EmDashRuntime {
|
|
|
2013
2420
|
trailingSlash?: "always" | "never" | "ignore";
|
|
2014
2421
|
},
|
|
2015
2422
|
): Promise<Map<string, SandboxedPluginInstance>> {
|
|
2016
|
-
// Return cached plugins if already loaded
|
|
2017
|
-
if (sandboxedPluginCache.size > 0) {
|
|
2018
|
-
return sandboxedPluginCache;
|
|
2019
|
-
}
|
|
2020
|
-
|
|
2021
2423
|
// Check if sandboxing is enabled
|
|
2022
2424
|
if (!deps.sandboxEnabled) {
|
|
2023
2425
|
return sandboxedPluginCache;
|
|
@@ -2030,6 +2432,8 @@ export class EmDashRuntime {
|
|
|
2030
2432
|
{
|
|
2031
2433
|
db,
|
|
2032
2434
|
beforeContentWrite: () => assertMediaUsageActivationWriteAllowed(db),
|
|
2435
|
+
taxonomyWrite: createTaxonomyAccessWithWrite(db),
|
|
2436
|
+
now: deps.now,
|
|
2033
2437
|
mediaStorage: mediaStorage
|
|
2034
2438
|
? {
|
|
2035
2439
|
upload: (opts) =>
|
|
@@ -2038,6 +2442,7 @@ export class EmDashRuntime {
|
|
|
2038
2442
|
body: opts.body,
|
|
2039
2443
|
contentType: opts.contentType,
|
|
2040
2444
|
}),
|
|
2445
|
+
download: (key) => mediaStorage.download(key),
|
|
2041
2446
|
delete: (key) => mediaStorage.delete(key),
|
|
2042
2447
|
}
|
|
2043
2448
|
: undefined,
|
|
@@ -2055,10 +2460,14 @@ export class EmDashRuntime {
|
|
|
2055
2460
|
// Warn regardless of whether there are plugins to load, so operators
|
|
2056
2461
|
// see the issue even if no marketplace plugins are installed yet.
|
|
2057
2462
|
if (!sandboxRunner.isAvailable()) {
|
|
2463
|
+
const reason = sandboxRunner.unavailableReason?.();
|
|
2058
2464
|
console.warn(
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2465
|
+
reason
|
|
2466
|
+
? `EmDash: Plugin sandbox is configured but not available on this platform: ${reason}. ` +
|
|
2467
|
+
"Sandboxed plugins will not be loaded."
|
|
2468
|
+
: "EmDash: Plugin sandbox is configured but not available on this platform. " +
|
|
2469
|
+
"Sandboxed plugins will not be loaded. " +
|
|
2470
|
+
"If using @emdash-cms/sandbox-workerd/sandbox, ensure workerd is installed.",
|
|
2062
2471
|
);
|
|
2063
2472
|
return sandboxedPluginCache;
|
|
2064
2473
|
}
|
|
@@ -2082,21 +2491,49 @@ export class EmDashRuntime {
|
|
|
2082
2491
|
}
|
|
2083
2492
|
|
|
2084
2493
|
try {
|
|
2494
|
+
const adminPages = entry.adminPages?.map((page) => ({
|
|
2495
|
+
path: page.path,
|
|
2496
|
+
label: page.label ?? page.path,
|
|
2497
|
+
icon: page.icon,
|
|
2498
|
+
}));
|
|
2499
|
+
const adminWidgets: PluginDashboardWidget[] | undefined = entry.adminWidgets?.map(
|
|
2500
|
+
(widget) => ({
|
|
2501
|
+
id: widget.id,
|
|
2502
|
+
title: widget.title,
|
|
2503
|
+
size:
|
|
2504
|
+
widget.size === "full" || widget.size === "half" || widget.size === "third"
|
|
2505
|
+
? widget.size
|
|
2506
|
+
: undefined,
|
|
2507
|
+
}),
|
|
2508
|
+
);
|
|
2509
|
+
const capabilities = normalizePluginCapabilities(entry.capabilities ?? []);
|
|
2510
|
+
|
|
2085
2511
|
// Build manifest from entry's declared config
|
|
2086
2512
|
const manifest: PluginManifest = {
|
|
2087
2513
|
id: entry.id,
|
|
2088
2514
|
version: entry.version,
|
|
2089
|
-
capabilities
|
|
2515
|
+
capabilities,
|
|
2090
2516
|
allowedHosts: entry.allowedHosts ?? [],
|
|
2091
2517
|
storage: entry.storage ?? {},
|
|
2092
|
-
|
|
2518
|
+
// Descriptors built before hook metadata was emitted must keep
|
|
2519
|
+
// working. Invoking an absent hook is a no-op in both runners.
|
|
2520
|
+
hooks: entry.hooks ?? [...HOOK_NAMES],
|
|
2093
2521
|
routes: entry.routes ?? [],
|
|
2094
|
-
admin: {
|
|
2522
|
+
admin: {
|
|
2523
|
+
pages: adminPages,
|
|
2524
|
+
widgets: adminWidgets,
|
|
2525
|
+
settingsSchema: entry.settingsSchema,
|
|
2526
|
+
portableTextBlocks: entry.portableTextBlocks,
|
|
2527
|
+
fieldWidgets: entry.fieldWidgets,
|
|
2528
|
+
editorPanels: entry.editorPanels,
|
|
2529
|
+
editorActions: entry.editorActions,
|
|
2530
|
+
},
|
|
2095
2531
|
mcp: entry.mcp,
|
|
2096
2532
|
};
|
|
2097
2533
|
|
|
2098
2534
|
const plugin = await sandboxRunner.load(manifest, entry.code);
|
|
2099
2535
|
sandboxedPluginCache.set(pluginKey, plugin);
|
|
2536
|
+
sandboxedManifestCache.set(pluginKey, manifest);
|
|
2100
2537
|
console.log(
|
|
2101
2538
|
`EmDash: Loaded sandboxed plugin ${pluginKey} with capabilities: [${manifest.capabilities.join(", ")}]`,
|
|
2102
2539
|
);
|
|
@@ -2154,6 +2591,8 @@ export class EmDashRuntime {
|
|
|
2154
2591
|
{
|
|
2155
2592
|
db,
|
|
2156
2593
|
beforeContentWrite: () => assertMediaUsageActivationWriteAllowed(db),
|
|
2594
|
+
taxonomyWrite: createTaxonomyAccessWithWrite(db),
|
|
2595
|
+
now: deps.now,
|
|
2157
2596
|
mediaStorage: {
|
|
2158
2597
|
upload: (opts) =>
|
|
2159
2598
|
storage.upload({
|
|
@@ -2161,6 +2600,7 @@ export class EmDashRuntime {
|
|
|
2161
2600
|
body: opts.body,
|
|
2162
2601
|
contentType: opts.contentType,
|
|
2163
2602
|
}),
|
|
2603
|
+
download: (key) => storage.download(key),
|
|
2164
2604
|
delete: (key) => storage.delete(key),
|
|
2165
2605
|
},
|
|
2166
2606
|
},
|
|
@@ -2206,6 +2646,7 @@ export class EmDashRuntime {
|
|
|
2206
2646
|
|
|
2207
2647
|
const loaded = await sandboxRunner.load(bundle.manifest, bundle.backendCode);
|
|
2208
2648
|
cache.set(pluginKey, loaded);
|
|
2649
|
+
sandboxedManifestCache.set(pluginKey, bundle.manifest);
|
|
2209
2650
|
keySet.add(pluginKey);
|
|
2210
2651
|
|
|
2211
2652
|
// Cache manifest admin config for getManifest()
|
|
@@ -2215,6 +2656,8 @@ export class EmDashRuntime {
|
|
|
2215
2656
|
admin: bundle.manifest.admin,
|
|
2216
2657
|
mcp: bundle.manifest.mcp,
|
|
2217
2658
|
storage: bundle.manifest.storage,
|
|
2659
|
+
allowedHosts: bundle.manifest.allowedHosts,
|
|
2660
|
+
capabilities: bundle.manifest.capabilities,
|
|
2218
2661
|
});
|
|
2219
2662
|
|
|
2220
2663
|
// Cache route metadata from manifest for auth decisions
|
|
@@ -2288,6 +2731,8 @@ export class EmDashRuntime {
|
|
|
2288
2731
|
admin: bundle.manifest.admin,
|
|
2289
2732
|
mcp: bundle.manifest.mcp,
|
|
2290
2733
|
storage: bundle.manifest.storage,
|
|
2734
|
+
allowedHosts: bundle.manifest.allowedHosts,
|
|
2735
|
+
capabilities: bundle.manifest.capabilities,
|
|
2291
2736
|
});
|
|
2292
2737
|
if (bundle.manifest.routes.length > 0) {
|
|
2293
2738
|
const routeMeta = new Map<string, RouteMeta>();
|
|
@@ -2324,6 +2769,8 @@ export class EmDashRuntime {
|
|
|
2324
2769
|
w.size === "full" || w.size === "half" || w.size === "third" ? w.size : undefined,
|
|
2325
2770
|
})),
|
|
2326
2771
|
settingsSchema: bundle.manifest.admin?.settingsSchema,
|
|
2772
|
+
editorPanels: bundle.manifest.admin?.editorPanels,
|
|
2773
|
+
editorActions: bundle.manifest.admin?.editorActions,
|
|
2327
2774
|
});
|
|
2328
2775
|
resolved.push(adapted);
|
|
2329
2776
|
console.log(
|
|
@@ -2457,6 +2904,8 @@ export class EmDashRuntime {
|
|
|
2457
2904
|
fieldTypes: string[];
|
|
2458
2905
|
elements?: Element[];
|
|
2459
2906
|
}>;
|
|
2907
|
+
editorPanels?: PluginManifest["admin"]["editorPanels"];
|
|
2908
|
+
editorActions?: PluginManifest["admin"]["editorActions"];
|
|
2460
2909
|
}
|
|
2461
2910
|
> = {};
|
|
2462
2911
|
|
|
@@ -2468,10 +2917,13 @@ export class EmDashRuntime {
|
|
|
2468
2917
|
const hasAdminEntry = !!plugin.admin?.entry;
|
|
2469
2918
|
const hasAdminPages = (plugin.admin?.pages?.length ?? 0) > 0;
|
|
2470
2919
|
const hasWidgets = (plugin.admin?.widgets?.length ?? 0) > 0;
|
|
2920
|
+
const hasEditorExtensions =
|
|
2921
|
+
(plugin.admin?.editorPanels?.length ?? 0) > 0 ||
|
|
2922
|
+
(plugin.admin?.editorActions?.length ?? 0) > 0;
|
|
2471
2923
|
let adminMode: "react" | "blocks" | "none" = "none";
|
|
2472
2924
|
if (hasAdminEntry) {
|
|
2473
2925
|
adminMode = "react";
|
|
2474
|
-
} else if (hasAdminPages || hasWidgets) {
|
|
2926
|
+
} else if (hasAdminPages || hasWidgets || hasEditorExtensions) {
|
|
2475
2927
|
adminMode = "blocks";
|
|
2476
2928
|
}
|
|
2477
2929
|
|
|
@@ -2483,6 +2935,8 @@ export class EmDashRuntime {
|
|
|
2483
2935
|
dashboardWidgets: plugin.admin?.widgets ?? [],
|
|
2484
2936
|
portableTextBlocks: plugin.admin?.portableTextBlocks,
|
|
2485
2937
|
fieldWidgets: plugin.admin?.fieldWidgets,
|
|
2938
|
+
editorPanels: plugin.admin?.editorPanels,
|
|
2939
|
+
editorActions: plugin.admin?.editorActions,
|
|
2486
2940
|
};
|
|
2487
2941
|
}
|
|
2488
2942
|
|
|
@@ -2493,6 +2947,8 @@ export class EmDashRuntime {
|
|
|
2493
2947
|
|
|
2494
2948
|
const hasAdminPages = (entry.adminPages?.length ?? 0) > 0;
|
|
2495
2949
|
const hasWidgets = (entry.adminWidgets?.length ?? 0) > 0;
|
|
2950
|
+
const hasEditorExtensions =
|
|
2951
|
+
(entry.editorPanels?.length ?? 0) > 0 || (entry.editorActions?.length ?? 0) > 0;
|
|
2496
2952
|
|
|
2497
2953
|
manifestPlugins[entry.id] = {
|
|
2498
2954
|
version: entry.version,
|
|
@@ -2502,11 +2958,13 @@ export class EmDashRuntime {
|
|
|
2502
2958
|
// contribute portableTextBlocks/fieldWidgets with adminMode "none" —
|
|
2503
2959
|
// the admin reads those from the manifest regardless, so don't gate
|
|
2504
2960
|
// admin contributions on `adminMode`.
|
|
2505
|
-
adminMode: hasAdminPages || hasWidgets ? "blocks" : "none",
|
|
2961
|
+
adminMode: hasAdminPages || hasWidgets || hasEditorExtensions ? "blocks" : "none",
|
|
2506
2962
|
adminPages: entry.adminPages ?? [],
|
|
2507
2963
|
dashboardWidgets: entry.adminWidgets ?? [],
|
|
2508
2964
|
portableTextBlocks: entry.portableTextBlocks,
|
|
2509
2965
|
fieldWidgets: entry.fieldWidgets,
|
|
2966
|
+
editorPanels: entry.editorPanels,
|
|
2967
|
+
editorActions: entry.editorActions,
|
|
2510
2968
|
};
|
|
2511
2969
|
}
|
|
2512
2970
|
|
|
@@ -2522,14 +2980,18 @@ export class EmDashRuntime {
|
|
|
2522
2980
|
const widgets = meta.admin?.widgets;
|
|
2523
2981
|
const hasAdminPages = (pages?.length ?? 0) > 0;
|
|
2524
2982
|
const hasWidgets = (widgets?.length ?? 0) > 0;
|
|
2983
|
+
const hasEditorExtensions =
|
|
2984
|
+
(meta.admin?.editorPanels?.length ?? 0) > 0 || (meta.admin?.editorActions?.length ?? 0) > 0;
|
|
2525
2985
|
|
|
2526
2986
|
manifestPlugins[pluginId] = {
|
|
2527
2987
|
version: meta.version,
|
|
2528
2988
|
enabled,
|
|
2529
2989
|
sandboxed: true,
|
|
2530
|
-
adminMode: hasAdminPages || hasWidgets ? "blocks" : "none",
|
|
2990
|
+
adminMode: hasAdminPages || hasWidgets || hasEditorExtensions ? "blocks" : "none",
|
|
2531
2991
|
adminPages: pages ?? [],
|
|
2532
2992
|
dashboardWidgets: widgets ?? [],
|
|
2993
|
+
editorPanels: meta.admin?.editorPanels,
|
|
2994
|
+
editorActions: meta.admin?.editorActions,
|
|
2533
2995
|
};
|
|
2534
2996
|
}
|
|
2535
2997
|
|
|
@@ -2599,11 +3061,19 @@ export class EmDashRuntime {
|
|
|
2599
3061
|
}
|
|
2600
3062
|
: undefined;
|
|
2601
3063
|
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
const registry =
|
|
3064
|
+
const registryConfig = resolveRegistryConfigForSandbox({
|
|
3065
|
+
registry: this.config.registry,
|
|
3066
|
+
experimentalRegistry: this.config.experimental?.registry,
|
|
3067
|
+
});
|
|
3068
|
+
const { registry, error: registryConfigurationError } = resolveManifestRegistryConfig(
|
|
3069
|
+
registryConfig.input,
|
|
3070
|
+
{ fieldPrefix: registryConfig.fieldPrefix },
|
|
3071
|
+
);
|
|
3072
|
+
if (registryConfigurationError) {
|
|
3073
|
+
console.error(
|
|
3074
|
+
`EmDash registry configuration error in ${registryConfigurationError.field} (${registryConfigurationError.code})`,
|
|
3075
|
+
);
|
|
3076
|
+
}
|
|
2607
3077
|
|
|
2608
3078
|
return {
|
|
2609
3079
|
version: VERSION,
|
|
@@ -2621,6 +3091,7 @@ export class EmDashRuntime {
|
|
|
2621
3091
|
},
|
|
2622
3092
|
marketplace: !!this.config.marketplace,
|
|
2623
3093
|
registry,
|
|
3094
|
+
registryConfigurationError,
|
|
2624
3095
|
};
|
|
2625
3096
|
}
|
|
2626
3097
|
|
|
@@ -2729,7 +3200,10 @@ export class EmDashRuntime {
|
|
|
2729
3200
|
*
|
|
2730
3201
|
* No-op when no draft exists or the response is an error.
|
|
2731
3202
|
*/
|
|
2732
|
-
private async hydrateDraftData<T>(
|
|
3203
|
+
private async hydrateDraftData<T>(
|
|
3204
|
+
result: T,
|
|
3205
|
+
options: { includeStagedSlug?: boolean; strict?: boolean } = {},
|
|
3206
|
+
): Promise<T> {
|
|
2733
3207
|
if (!result || typeof result !== "object") return result;
|
|
2734
3208
|
// eslint-disable-next-line typescript/no-unsafe-type-assertion -- shape probed below
|
|
2735
3209
|
const r = result as {
|
|
@@ -2742,7 +3216,10 @@ export class EmDashRuntime {
|
|
|
2742
3216
|
if (!draftRevisionId) return result;
|
|
2743
3217
|
try {
|
|
2744
3218
|
const revision = await new RevisionRepository(this.db).findById(draftRevisionId);
|
|
2745
|
-
if (!revision)
|
|
3219
|
+
if (!revision) {
|
|
3220
|
+
if (options.strict) throw new Error(`Draft revision not found: ${draftRevisionId}`);
|
|
3221
|
+
return result;
|
|
3222
|
+
}
|
|
2746
3223
|
const liveData =
|
|
2747
3224
|
item.data && typeof item.data === "object"
|
|
2748
3225
|
? // eslint-disable-next-line typescript/no-unsafe-type-assertion -- narrowed to object above
|
|
@@ -2769,10 +3246,18 @@ export class EmDashRuntime {
|
|
|
2769
3246
|
// eslint-disable-next-line typescript/no-unsafe-type-assertion -- shape preserved; result has been narrowed to the {success,data:{item}} envelope
|
|
2770
3247
|
data: {
|
|
2771
3248
|
...r.data,
|
|
2772
|
-
item: {
|
|
3249
|
+
item: {
|
|
3250
|
+
...item,
|
|
3251
|
+
...(options.includeStagedSlug && typeof revision.data._slug === "string"
|
|
3252
|
+
? { slug: revision.data._slug }
|
|
3253
|
+
: {}),
|
|
3254
|
+
data: mergedData,
|
|
3255
|
+
liveData,
|
|
3256
|
+
},
|
|
2773
3257
|
},
|
|
2774
3258
|
};
|
|
2775
3259
|
} catch (error) {
|
|
3260
|
+
if (options.strict) throw error;
|
|
2776
3261
|
// Non-fatal — fall back to the unhydrated response. Log so the
|
|
2777
3262
|
// failure isn't completely silent (the response will look stale
|
|
2778
3263
|
// to the caller but no error is raised).
|
|
@@ -2785,6 +3270,7 @@ export class EmDashRuntime {
|
|
|
2785
3270
|
collection: string,
|
|
2786
3271
|
body: {
|
|
2787
3272
|
data: Record<string, unknown>;
|
|
3273
|
+
seo?: ContentWriteInput["seo"];
|
|
2788
3274
|
slug?: string | null;
|
|
2789
3275
|
status?: string;
|
|
2790
3276
|
authorId?: string;
|
|
@@ -2792,23 +3278,87 @@ export class EmDashRuntime {
|
|
|
2792
3278
|
locale?: string;
|
|
2793
3279
|
translationOf?: string;
|
|
2794
3280
|
taxonomies?: Record<string, string[]>;
|
|
3281
|
+
actor?: ActorInfo;
|
|
2795
3282
|
},
|
|
3283
|
+
options: { skipSaveHooks?: boolean; excludeAfterSavePluginId?: string } = {},
|
|
2796
3284
|
) {
|
|
3285
|
+
const actor = body.actor ? { ...body.actor } : undefined;
|
|
3286
|
+
let locale: string;
|
|
3287
|
+
try {
|
|
3288
|
+
locale = resolveContentCreateLocale(body.locale);
|
|
3289
|
+
} catch (error) {
|
|
3290
|
+
return {
|
|
3291
|
+
success: false as const,
|
|
3292
|
+
error: {
|
|
3293
|
+
code: "VALIDATION_ERROR",
|
|
3294
|
+
message: error instanceof Error ? error.message : "Invalid locale",
|
|
3295
|
+
},
|
|
3296
|
+
};
|
|
3297
|
+
}
|
|
3298
|
+
|
|
3299
|
+
let translationSource: ContentItemInternal | null = null;
|
|
3300
|
+
let sharedFieldSlugs: string[] = [];
|
|
3301
|
+
if (body.translationOf) {
|
|
3302
|
+
const repo = new ContentRepository(this.db);
|
|
3303
|
+
const source = await repo.findById(collection, body.translationOf);
|
|
3304
|
+
if (!source) {
|
|
3305
|
+
return {
|
|
3306
|
+
success: false as const,
|
|
3307
|
+
error: { code: "NOT_FOUND", message: "Translation source content not found" },
|
|
3308
|
+
};
|
|
3309
|
+
}
|
|
3310
|
+
translationSource = source;
|
|
3311
|
+
sharedFieldSlugs = (
|
|
3312
|
+
await this.db
|
|
3313
|
+
.selectFrom("_emdash_fields as field")
|
|
3314
|
+
.innerJoin("_emdash_collections as collection", "collection.id", "field.collection_id")
|
|
3315
|
+
.select("field.slug")
|
|
3316
|
+
.where("collection.slug", "=", collection)
|
|
3317
|
+
.where("field.translatable", "=", 0)
|
|
3318
|
+
.execute()
|
|
3319
|
+
).map((field) => field.slug);
|
|
3320
|
+
const siblings = await repo.findTranslations(
|
|
3321
|
+
collection,
|
|
3322
|
+
source.translationGroup ?? source.id,
|
|
3323
|
+
);
|
|
3324
|
+
if (siblings.some((sibling) => sibling.locale?.toLowerCase() === locale.toLowerCase())) {
|
|
3325
|
+
return {
|
|
3326
|
+
success: false as const,
|
|
3327
|
+
error: {
|
|
3328
|
+
code: "CONFLICT",
|
|
3329
|
+
message: `Translation already exists in locale "${locale}" for this content item`,
|
|
3330
|
+
},
|
|
3331
|
+
};
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
|
|
2797
3335
|
// Run beforeSave hooks (trusted plugins)
|
|
2798
3336
|
let processedData = body.data;
|
|
2799
|
-
if (this.hooks.hasHooks("content:beforeSave")) {
|
|
3337
|
+
if (!options.skipSaveHooks && this.hooks.hasHooks("content:beforeSave")) {
|
|
2800
3338
|
try {
|
|
2801
|
-
const hookResult = await this.hooks.runContentBeforeSave(
|
|
3339
|
+
const hookResult = await this.hooks.runContentBeforeSave(
|
|
3340
|
+
body.data,
|
|
3341
|
+
collection,
|
|
3342
|
+
true,
|
|
3343
|
+
undefined,
|
|
3344
|
+
actor,
|
|
3345
|
+
);
|
|
2802
3346
|
processedData = hookResult.content;
|
|
2803
3347
|
} catch (error) {
|
|
2804
3348
|
return beforeSaveFailure(error);
|
|
2805
3349
|
}
|
|
2806
3350
|
}
|
|
2807
3351
|
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
3352
|
+
if (translationSource) {
|
|
3353
|
+
processedData = { ...processedData };
|
|
3354
|
+
for (const slug of sharedFieldSlugs) {
|
|
3355
|
+
if (Object.hasOwn(translationSource.data, slug)) {
|
|
3356
|
+
processedData[slug] = translationSource.data[slug];
|
|
3357
|
+
} else {
|
|
3358
|
+
delete processedData[slug];
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
2812
3362
|
|
|
2813
3363
|
// Normalize media fields (fill dimensions, storageKey, etc.)
|
|
2814
3364
|
processedData = await this.normalizeMediaFields(collection, processedData);
|
|
@@ -2831,6 +3381,7 @@ export class EmDashRuntime {
|
|
|
2831
3381
|
const result = await handleContentCreate(this.db, collection, {
|
|
2832
3382
|
...body,
|
|
2833
3383
|
data: processedData,
|
|
3384
|
+
locale,
|
|
2834
3385
|
authorId: body.authorId,
|
|
2835
3386
|
bylines: body.bylines,
|
|
2836
3387
|
});
|
|
@@ -2839,8 +3390,14 @@ export class EmDashRuntime {
|
|
|
2839
3390
|
}
|
|
2840
3391
|
|
|
2841
3392
|
// Run afterSave hooks (fire-and-forget)
|
|
2842
|
-
if (result.success && result.data) {
|
|
2843
|
-
this.runAfterSaveHooks(
|
|
3393
|
+
if (!options.skipSaveHooks && result.success && result.data) {
|
|
3394
|
+
this.runAfterSaveHooks(
|
|
3395
|
+
contentItemToRecord(result.data.item),
|
|
3396
|
+
collection,
|
|
3397
|
+
true,
|
|
3398
|
+
actor,
|
|
3399
|
+
options.excludeAfterSavePluginId,
|
|
3400
|
+
);
|
|
2844
3401
|
}
|
|
2845
3402
|
|
|
2846
3403
|
return result;
|
|
@@ -2868,8 +3425,15 @@ export class EmDashRuntime {
|
|
|
2868
3425
|
/** Replace the previous autosave revision after staging this save. */
|
|
2869
3426
|
skipRevision?: boolean;
|
|
2870
3427
|
_rev?: string;
|
|
3428
|
+
/**
|
|
3429
|
+
* Acting user for this save. Used for revision attribution and
|
|
3430
|
+
* passed to content hooks; never changes entry ownership.
|
|
3431
|
+
*/
|
|
3432
|
+
actor?: ActorInfo;
|
|
2871
3433
|
},
|
|
2872
3434
|
) {
|
|
3435
|
+
const actor = body.actor ? { ...body.actor } : undefined;
|
|
3436
|
+
|
|
2873
3437
|
// Resolve slug → ID if needed (before any lookups)
|
|
2874
3438
|
const repo = new ContentRepository(this.db);
|
|
2875
3439
|
const resolvedItem = await repo.findByIdOrSlug(collection, id, body.locale);
|
|
@@ -2893,7 +3457,14 @@ export class EmDashRuntime {
|
|
|
2893
3457
|
};
|
|
2894
3458
|
}
|
|
2895
3459
|
}
|
|
2896
|
-
const { _rev: _discardedRev, ...bodyWithoutRev } = body;
|
|
3460
|
+
const { _rev: _discardedRev, actor: _discardedActor, ...bodyWithoutRev } = body;
|
|
3461
|
+
|
|
3462
|
+
// Loaded once and threaded through normalization, the stale-key drop and the draft
|
|
3463
|
+
// merge below: each of those needs the field list and the registry does not cache.
|
|
3464
|
+
const collectionInfo = bodyWithoutRev.data
|
|
3465
|
+
? await this.schemaRegistry.getCollectionWithFields(collection).catch(() => null)
|
|
3466
|
+
: null;
|
|
3467
|
+
const knownFieldSlugs = new Set((collectionInfo?.fields ?? []).map((f) => f.slug));
|
|
2897
3468
|
|
|
2898
3469
|
// Run beforeSave hooks if data is provided
|
|
2899
3470
|
let processedData = bodyWithoutRev.data;
|
|
@@ -2905,6 +3476,7 @@ export class EmDashRuntime {
|
|
|
2905
3476
|
collection,
|
|
2906
3477
|
false,
|
|
2907
3478
|
resolvedItem?.id,
|
|
3479
|
+
actor,
|
|
2908
3480
|
);
|
|
2909
3481
|
processedData = hookResult.content;
|
|
2910
3482
|
} catch (error) {
|
|
@@ -2912,18 +3484,19 @@ export class EmDashRuntime {
|
|
|
2912
3484
|
}
|
|
2913
3485
|
}
|
|
2914
3486
|
|
|
2915
|
-
// Run sandboxed beforeSave hooks
|
|
2916
|
-
const sandboxResult = await this.runSandboxedBeforeSave(
|
|
2917
|
-
processedData!,
|
|
2918
|
-
collection,
|
|
2919
|
-
false,
|
|
2920
|
-
resolvedItem?.id,
|
|
2921
|
-
);
|
|
2922
|
-
if (!sandboxResult.success) return sandboxResult;
|
|
2923
|
-
processedData = sandboxResult.data;
|
|
2924
|
-
|
|
2925
3487
|
// Normalize media fields (fill dimensions, storageKey, etc.)
|
|
2926
|
-
processedData = await this.normalizeMediaFields(collection, processedData);
|
|
3488
|
+
processedData = await this.normalizeMediaFields(collection, processedData!, collectionInfo);
|
|
3489
|
+
|
|
3490
|
+
// Drop unknown field keys the entry already stores (e.g. a deleted field
|
|
3491
|
+
// stranded in a draft revision) before validation, while still rejecting
|
|
3492
|
+
// unknown keys the entry has never stored.
|
|
3493
|
+
if (collectionInfo?.fields) {
|
|
3494
|
+
processedData = await this.dropUnknownKeysAlreadyStored(
|
|
3495
|
+
processedData,
|
|
3496
|
+
resolvedItem,
|
|
3497
|
+
knownFieldSlugs,
|
|
3498
|
+
);
|
|
3499
|
+
}
|
|
2927
3500
|
|
|
2928
3501
|
// Validate field-level shape BEFORE the draft-revision write so
|
|
2929
3502
|
// invalid updates can't silently land in revision history.
|
|
@@ -2945,7 +3518,6 @@ export class EmDashRuntime {
|
|
|
2945
3518
|
let usesDraftRevisions = false;
|
|
2946
3519
|
let draftStorageChanged = false;
|
|
2947
3520
|
if (processedData) {
|
|
2948
|
-
const collectionInfo = await this.schemaRegistry.getCollectionWithFields(collection);
|
|
2949
3521
|
if (collectionInfo?.supports?.includes("revisions")) {
|
|
2950
3522
|
usesDraftRevisions = true;
|
|
2951
3523
|
const revisionRepo = new RevisionRepository(this.db);
|
|
@@ -2960,7 +3532,12 @@ export class EmDashRuntime {
|
|
|
2960
3532
|
baseData = existing.data;
|
|
2961
3533
|
}
|
|
2962
3534
|
|
|
2963
|
-
|
|
3535
|
+
// Written without the keys the collection has no field for, so an entry
|
|
3536
|
+
// carrying a deleted field's value sheds it on its next save instead of
|
|
3537
|
+
// carrying it through every revision that follows.
|
|
3538
|
+
const mergedData = collectionInfo?.fields
|
|
3539
|
+
? keepKnownFields({ ...baseData, ...processedData }, knownFieldSlugs)
|
|
3540
|
+
: { ...baseData, ...processedData };
|
|
2964
3541
|
if (bodyWithoutRev.slug !== undefined) {
|
|
2965
3542
|
mergedData._slug = bodyWithoutRev.slug;
|
|
2966
3543
|
}
|
|
@@ -2969,7 +3546,7 @@ export class EmDashRuntime {
|
|
|
2969
3546
|
collection,
|
|
2970
3547
|
entryId: resolvedId,
|
|
2971
3548
|
data: mergedData,
|
|
2972
|
-
authorId:
|
|
3549
|
+
authorId: actor?.id,
|
|
2973
3550
|
});
|
|
2974
3551
|
|
|
2975
3552
|
let staged: boolean;
|
|
@@ -3070,31 +3647,14 @@ export class EmDashRuntime {
|
|
|
3070
3647
|
if (hydrated.success && hydrated.data) {
|
|
3071
3648
|
const contentIdsToRefresh = [resolvedId];
|
|
3072
3649
|
if (!usesDraftRevisions && processedData) {
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
)),
|
|
3082
|
-
);
|
|
3083
|
-
} catch (error) {
|
|
3084
|
-
console.error(
|
|
3085
|
-
`[media-usage] Failed to discover synced i18n siblings for ${collection}/${resolvedId}:`,
|
|
3086
|
-
error,
|
|
3087
|
-
);
|
|
3088
|
-
try {
|
|
3089
|
-
await markContentMediaUsageCollectionStale(
|
|
3090
|
-
this.db,
|
|
3091
|
-
collection,
|
|
3092
|
-
"CONTENT_USAGE_REFRESH_ERROR",
|
|
3093
|
-
);
|
|
3094
|
-
} catch (staleError) {
|
|
3095
|
-
console.error(`[media-usage] Failed to mark ${collection} stale:`, staleError);
|
|
3096
|
-
}
|
|
3097
|
-
}
|
|
3650
|
+
contentIdsToRefresh.push(
|
|
3651
|
+
...(await this.findSyncedSiblingsForUsageRefresh(
|
|
3652
|
+
collection,
|
|
3653
|
+
resolvedId,
|
|
3654
|
+
hydrated.data.item.translationGroup,
|
|
3655
|
+
processedData,
|
|
3656
|
+
)),
|
|
3657
|
+
);
|
|
3098
3658
|
}
|
|
3099
3659
|
await this.refreshContentUsageAfterSuccessfulWrite(collection, contentIdsToRefresh);
|
|
3100
3660
|
} else if (draftStorageChanged) {
|
|
@@ -3107,7 +3667,7 @@ export class EmDashRuntime {
|
|
|
3107
3667
|
|
|
3108
3668
|
// Run afterSave hooks (fire-and-forget)
|
|
3109
3669
|
if (hydrated.success && hydrated.data) {
|
|
3110
|
-
this.runAfterSaveHooks(contentItemToRecord(hydrated.data.item), collection, false);
|
|
3670
|
+
this.runAfterSaveHooks(contentItemToRecord(hydrated.data.item), collection, false, actor);
|
|
3111
3671
|
}
|
|
3112
3672
|
|
|
3113
3673
|
if (hydrated.success) {
|
|
@@ -3131,21 +3691,12 @@ export class EmDashRuntime {
|
|
|
3131
3691
|
}
|
|
3132
3692
|
}
|
|
3133
3693
|
|
|
3134
|
-
|
|
3135
|
-
const sandboxAllowed = await this.runSandboxedBeforeDelete(id, collection);
|
|
3136
|
-
if (!sandboxAllowed) {
|
|
3137
|
-
return {
|
|
3138
|
-
success: false,
|
|
3139
|
-
error: {
|
|
3140
|
-
code: "DELETE_BLOCKED",
|
|
3141
|
-
message: "Delete blocked by sandboxed plugin hook",
|
|
3142
|
-
},
|
|
3143
|
-
};
|
|
3144
|
-
}
|
|
3694
|
+
const policyRejectionRevision = await this.getScheduledPolicyRejectionRevision(collection, id);
|
|
3145
3695
|
|
|
3146
3696
|
// Delete the content
|
|
3147
3697
|
const result = await handleContentDelete(this.db, collection, id);
|
|
3148
3698
|
if (result.success) {
|
|
3699
|
+
await this.clearScheduledPolicyRejection(collection, result.data.id, policyRejectionRevision);
|
|
3149
3700
|
await this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.id]);
|
|
3150
3701
|
}
|
|
3151
3702
|
|
|
@@ -3168,23 +3719,38 @@ export class EmDashRuntime {
|
|
|
3168
3719
|
return handleContentListTrashed(this.db, collection, params);
|
|
3169
3720
|
}
|
|
3170
3721
|
|
|
3171
|
-
async handleContentRestore(
|
|
3172
|
-
|
|
3722
|
+
async handleContentRestore(
|
|
3723
|
+
collection: string,
|
|
3724
|
+
id: string,
|
|
3725
|
+
options: {
|
|
3726
|
+
_rev?: string;
|
|
3727
|
+
origin?: ContentActionOrigin;
|
|
3728
|
+
pluginInvocationId?: string;
|
|
3729
|
+
} = {},
|
|
3730
|
+
) {
|
|
3731
|
+
const result = await handleContentRestore(this.db, collection, id, { _rev: options._rev });
|
|
3173
3732
|
if (result.success && result.data) {
|
|
3174
3733
|
await this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.item.id]);
|
|
3175
3734
|
}
|
|
3176
3735
|
|
|
3177
3736
|
// Run afterRestore hooks (fire-and-forget)
|
|
3178
3737
|
if (result.success) {
|
|
3179
|
-
this.runAfterRestoreHooks(
|
|
3738
|
+
this.runAfterRestoreHooks(
|
|
3739
|
+
contentItemToRecord(result.data.item),
|
|
3740
|
+
collection,
|
|
3741
|
+
options.origin?.source === "plugin" ? options.origin.pluginId : undefined,
|
|
3742
|
+
options.pluginInvocationId,
|
|
3743
|
+
);
|
|
3180
3744
|
}
|
|
3181
3745
|
|
|
3182
3746
|
return result;
|
|
3183
3747
|
}
|
|
3184
3748
|
|
|
3185
3749
|
async handleContentPermanentDelete(collection: string, id: string) {
|
|
3750
|
+
const policyRejectionRevision = await this.getScheduledPolicyRejectionRevision(collection, id);
|
|
3186
3751
|
const result = await handleContentPermanentDelete(this.db, collection, id);
|
|
3187
3752
|
if (result.success) {
|
|
3753
|
+
await this.clearScheduledPolicyRejection(collection, result.data.id, policyRejectionRevision);
|
|
3188
3754
|
await this.deleteContentUsageAfterSuccessfulPermanentDelete(collection, result.data.id);
|
|
3189
3755
|
}
|
|
3190
3756
|
|
|
@@ -3212,66 +3778,565 @@ export class EmDashRuntime {
|
|
|
3212
3778
|
// Publishing & Scheduling Handlers
|
|
3213
3779
|
// =========================================================================
|
|
3214
3780
|
|
|
3215
|
-
|
|
3781
|
+
private pluginVersionedResult(
|
|
3782
|
+
result:
|
|
3783
|
+
| { success: true; data: { item: ContentItemInternal; _rev?: string } }
|
|
3784
|
+
| { success: false; error: { code: string; message: string } },
|
|
3785
|
+
): VersionedContentItem {
|
|
3786
|
+
if (!result.success) {
|
|
3787
|
+
throw Object.assign(new Error(result.error.message), { code: result.error.code });
|
|
3788
|
+
}
|
|
3789
|
+
if (!result.data._rev) throw new Error("Content action returned no revision");
|
|
3790
|
+
return { item: toPluginContentItem(result.data.item), _rev: result.data._rev };
|
|
3791
|
+
}
|
|
3792
|
+
|
|
3793
|
+
private async runPluginContentAction<T>(
|
|
3794
|
+
pluginId: string,
|
|
3795
|
+
action: string,
|
|
3216
3796
|
collection: string,
|
|
3217
3797
|
id: string,
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3798
|
+
fn: (resolvedId: string) => Promise<T>,
|
|
3799
|
+
invocationId?: string,
|
|
3800
|
+
invalidateContentCache?: PluginContentCacheInvalidator,
|
|
3801
|
+
): Promise<T> {
|
|
3802
|
+
let invocationInvalidator: PluginContentCacheInvalidator | undefined;
|
|
3803
|
+
if (invocationId) {
|
|
3804
|
+
const invocationKey = this.pluginInvocationKey(pluginId, invocationId);
|
|
3805
|
+
if (
|
|
3806
|
+
!this.activePluginInvocations.has(invocationKey) &&
|
|
3807
|
+
!this.releasedPluginInvocations.has(invocationKey)
|
|
3808
|
+
) {
|
|
3809
|
+
throw Object.assign(new Error("Plugin content action invocation has expired"), {
|
|
3810
|
+
code: "CONTENT_ACTION_INVOCATION_EXPIRED",
|
|
3811
|
+
});
|
|
3812
|
+
}
|
|
3813
|
+
invocationInvalidator = this.pluginInvocationCacheInvalidators.get(invocationKey);
|
|
3228
3814
|
}
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3815
|
+
await assertMediaUsageActivationWriteAllowed(this.db);
|
|
3816
|
+
const repo = new ContentRepository(this.db);
|
|
3817
|
+
const item =
|
|
3818
|
+
action === "restore"
|
|
3819
|
+
? await repo.findByIdOrSlugIncludingTrashed(collection, id)
|
|
3820
|
+
: await repo.findByIdOrSlug(collection, id);
|
|
3821
|
+
const resolvedId = item?.id ?? id;
|
|
3822
|
+
const key = this.pluginContentActionKey(collection, resolvedId);
|
|
3823
|
+
if ((this.activePluginContentActions.get(key) ?? 0) > 0) {
|
|
3824
|
+
throw Object.assign(new Error("Plugin content action re-entered itself"), {
|
|
3825
|
+
code: "CONTENT_ACTION_REENTRANT",
|
|
3826
|
+
});
|
|
3233
3827
|
}
|
|
3234
|
-
|
|
3235
|
-
|
|
3828
|
+
this.retainPluginContentAction(key);
|
|
3829
|
+
try {
|
|
3830
|
+
const result = await fn(resolvedId);
|
|
3831
|
+
const invalidator =
|
|
3832
|
+
invalidateContentCache ?? invocationInvalidator ?? this.pluginContentCacheInvalidator;
|
|
3833
|
+
if (invalidator) {
|
|
3834
|
+
try {
|
|
3835
|
+
await invalidator([collection, resolvedId]);
|
|
3836
|
+
} catch (error) {
|
|
3837
|
+
console.error(
|
|
3838
|
+
`[plugin-content-action] Cache invalidation failed for ${collection}/${resolvedId}:`,
|
|
3839
|
+
error,
|
|
3840
|
+
);
|
|
3841
|
+
}
|
|
3842
|
+
}
|
|
3843
|
+
return result;
|
|
3844
|
+
} finally {
|
|
3845
|
+
this.releasePluginContentAction(key);
|
|
3846
|
+
}
|
|
3847
|
+
}
|
|
3848
|
+
|
|
3849
|
+
private pluginContentActionKey(collection: string, id: string): string {
|
|
3850
|
+
return JSON.stringify([collection, id]);
|
|
3851
|
+
}
|
|
3852
|
+
|
|
3853
|
+
private retainPluginContentAction(key: string): void {
|
|
3854
|
+
this.activePluginContentActions.set(key, (this.activePluginContentActions.get(key) ?? 0) + 1);
|
|
3855
|
+
}
|
|
3856
|
+
|
|
3857
|
+
private releasePluginContentAction(key: string): void {
|
|
3858
|
+
const remaining = (this.activePluginContentActions.get(key) ?? 1) - 1;
|
|
3859
|
+
if (remaining > 0) this.activePluginContentActions.set(key, remaining);
|
|
3860
|
+
else this.activePluginContentActions.delete(key);
|
|
3236
3861
|
}
|
|
3237
3862
|
|
|
3238
|
-
async
|
|
3239
|
-
|
|
3863
|
+
private async pluginGetVersioned(
|
|
3864
|
+
_pluginId: string,
|
|
3865
|
+
collection: string,
|
|
3866
|
+
id: string,
|
|
3867
|
+
): Promise<VersionedContentItem | null> {
|
|
3868
|
+
const result = await this.handleContentGet(collection, id);
|
|
3869
|
+
if (!result.success) {
|
|
3870
|
+
if (result.error.code === "NOT_FOUND") return null;
|
|
3871
|
+
throw Object.assign(new Error(result.error.message), { code: result.error.code });
|
|
3872
|
+
}
|
|
3873
|
+
if (!result.data._rev) throw new Error("Content read returned no revision");
|
|
3874
|
+
return { item: toPluginContentItem(result.data.item), _rev: result.data._rev };
|
|
3875
|
+
}
|
|
3876
|
+
|
|
3877
|
+
private async pluginGetTrashedVersioned(
|
|
3878
|
+
_pluginId: string,
|
|
3879
|
+
collection: string,
|
|
3880
|
+
id: string,
|
|
3881
|
+
): Promise<VersionedContentItem | null> {
|
|
3882
|
+
const result = await this.handleContentGetIncludingTrashed(collection, id);
|
|
3883
|
+
if (!result.success) {
|
|
3884
|
+
if (result.error.code === "NOT_FOUND") return null;
|
|
3885
|
+
throw Object.assign(new Error(result.error.message), { code: result.error.code });
|
|
3886
|
+
}
|
|
3887
|
+
if (!(await new ContentRepository(this.db).isTrashed(collection, result.data.item.id))) {
|
|
3888
|
+
return null;
|
|
3889
|
+
}
|
|
3890
|
+
if (!result.data._rev) throw new Error("Trashed content read returned no revision");
|
|
3891
|
+
return { item: toPluginContentItem(result.data.item), _rev: result.data._rev };
|
|
3892
|
+
}
|
|
3893
|
+
|
|
3894
|
+
private pluginPublish(
|
|
3895
|
+
pluginId: string,
|
|
3896
|
+
collection: string,
|
|
3897
|
+
id: string,
|
|
3898
|
+
options: { _rev: string },
|
|
3899
|
+
invocationId?: string,
|
|
3900
|
+
invalidateContentCache?: PluginContentCacheInvalidator,
|
|
3901
|
+
): Promise<VersionedContentItem> {
|
|
3902
|
+
return this.runPluginContentAction(
|
|
3903
|
+
pluginId,
|
|
3904
|
+
"publish",
|
|
3905
|
+
collection,
|
|
3906
|
+
id,
|
|
3907
|
+
async (resolvedId) =>
|
|
3908
|
+
this.pluginVersionedResult(
|
|
3909
|
+
await this.handleContentPublish(collection, resolvedId, {
|
|
3910
|
+
_rev: options._rev,
|
|
3911
|
+
origin: { source: "plugin", pluginId },
|
|
3912
|
+
pluginInvocationId: invocationId,
|
|
3913
|
+
}),
|
|
3914
|
+
),
|
|
3915
|
+
invocationId,
|
|
3916
|
+
invalidateContentCache,
|
|
3917
|
+
);
|
|
3918
|
+
}
|
|
3919
|
+
|
|
3920
|
+
private pluginUnpublish(
|
|
3921
|
+
pluginId: string,
|
|
3922
|
+
collection: string,
|
|
3923
|
+
id: string,
|
|
3924
|
+
options: { _rev: string },
|
|
3925
|
+
invocationId?: string,
|
|
3926
|
+
invalidateContentCache?: PluginContentCacheInvalidator,
|
|
3927
|
+
): Promise<VersionedContentItem> {
|
|
3928
|
+
return this.runPluginContentAction(
|
|
3929
|
+
pluginId,
|
|
3930
|
+
"unpublish",
|
|
3931
|
+
collection,
|
|
3932
|
+
id,
|
|
3933
|
+
async (resolvedId) =>
|
|
3934
|
+
this.pluginVersionedResult(
|
|
3935
|
+
await this.handleContentUnpublish(collection, resolvedId, {
|
|
3936
|
+
_rev: options._rev,
|
|
3937
|
+
origin: { source: "plugin", pluginId },
|
|
3938
|
+
pluginInvocationId: invocationId,
|
|
3939
|
+
}),
|
|
3940
|
+
),
|
|
3941
|
+
invocationId,
|
|
3942
|
+
invalidateContentCache,
|
|
3943
|
+
);
|
|
3944
|
+
}
|
|
3945
|
+
|
|
3946
|
+
private pluginSchedule(
|
|
3947
|
+
pluginId: string,
|
|
3948
|
+
collection: string,
|
|
3949
|
+
id: string,
|
|
3950
|
+
options: { scheduledAt: string; _rev: string },
|
|
3951
|
+
invocationId?: string,
|
|
3952
|
+
invalidateContentCache?: PluginContentCacheInvalidator,
|
|
3953
|
+
): Promise<VersionedContentItem> {
|
|
3954
|
+
return this.runPluginContentAction(
|
|
3955
|
+
pluginId,
|
|
3956
|
+
"schedule",
|
|
3957
|
+
collection,
|
|
3958
|
+
id,
|
|
3959
|
+
async (resolvedId) =>
|
|
3960
|
+
this.pluginVersionedResult(
|
|
3961
|
+
await this.handleContentSchedule(collection, resolvedId, options.scheduledAt, {
|
|
3962
|
+
_rev: options._rev,
|
|
3963
|
+
origin: { source: "plugin", pluginId },
|
|
3964
|
+
pluginInvocationId: invocationId,
|
|
3965
|
+
}),
|
|
3966
|
+
),
|
|
3967
|
+
invocationId,
|
|
3968
|
+
invalidateContentCache,
|
|
3969
|
+
);
|
|
3970
|
+
}
|
|
3971
|
+
|
|
3972
|
+
private pluginUnschedule(
|
|
3973
|
+
pluginId: string,
|
|
3974
|
+
collection: string,
|
|
3975
|
+
id: string,
|
|
3976
|
+
options: { _rev: string },
|
|
3977
|
+
invocationId?: string,
|
|
3978
|
+
invalidateContentCache?: PluginContentCacheInvalidator,
|
|
3979
|
+
): Promise<VersionedContentItem> {
|
|
3980
|
+
return this.runPluginContentAction(
|
|
3981
|
+
pluginId,
|
|
3982
|
+
"unschedule",
|
|
3983
|
+
collection,
|
|
3984
|
+
id,
|
|
3985
|
+
async (resolvedId) =>
|
|
3986
|
+
this.pluginVersionedResult(
|
|
3987
|
+
await this.handleContentUnschedule(collection, resolvedId, {
|
|
3988
|
+
...options,
|
|
3989
|
+
origin: { source: "plugin", pluginId },
|
|
3990
|
+
pluginInvocationId: invocationId,
|
|
3991
|
+
}),
|
|
3992
|
+
),
|
|
3993
|
+
invocationId,
|
|
3994
|
+
invalidateContentCache,
|
|
3995
|
+
);
|
|
3996
|
+
}
|
|
3997
|
+
|
|
3998
|
+
private pluginRestore(
|
|
3999
|
+
pluginId: string,
|
|
4000
|
+
collection: string,
|
|
4001
|
+
id: string,
|
|
4002
|
+
options: { _rev: string },
|
|
4003
|
+
invocationId?: string,
|
|
4004
|
+
invalidateContentCache?: PluginContentCacheInvalidator,
|
|
4005
|
+
): Promise<VersionedContentItem> {
|
|
4006
|
+
return this.runPluginContentAction(
|
|
4007
|
+
pluginId,
|
|
4008
|
+
"restore",
|
|
4009
|
+
collection,
|
|
4010
|
+
id,
|
|
4011
|
+
async (resolvedId) =>
|
|
4012
|
+
this.pluginVersionedResult(
|
|
4013
|
+
await this.handleContentRestore(collection, resolvedId, {
|
|
4014
|
+
...options,
|
|
4015
|
+
origin: { source: "plugin", pluginId },
|
|
4016
|
+
pluginInvocationId: invocationId,
|
|
4017
|
+
}),
|
|
4018
|
+
),
|
|
4019
|
+
invocationId,
|
|
4020
|
+
invalidateContentCache,
|
|
4021
|
+
);
|
|
4022
|
+
}
|
|
4023
|
+
|
|
4024
|
+
private async checkContentPolicy(
|
|
4025
|
+
name: ContentPolicyHookName,
|
|
4026
|
+
collection: string,
|
|
4027
|
+
id: string,
|
|
4028
|
+
options: ContentPolicyMutationOptions,
|
|
4029
|
+
rejectionCode: "PUBLISH_REJECTED" | "SCHEDULE_REJECTED" | "UNPUBLISH_REJECTED",
|
|
4030
|
+
failureCode: "CONTENT_PUBLISH_ERROR" | "CONTENT_SCHEDULE_ERROR" | "CONTENT_UNPUBLISH_ERROR",
|
|
4031
|
+
scheduledAt?: string,
|
|
4032
|
+
): Promise<ContentPolicyCheck> {
|
|
4033
|
+
let current = await handleContentGet(this.db, collection, id);
|
|
4034
|
+
if (!current.success) {
|
|
4035
|
+
return {
|
|
4036
|
+
allowed: false,
|
|
4037
|
+
revision: options._rev ?? "",
|
|
4038
|
+
error: current.error,
|
|
4039
|
+
};
|
|
4040
|
+
}
|
|
4041
|
+
|
|
4042
|
+
const revision = current.data._rev;
|
|
4043
|
+
if (!revision) {
|
|
4044
|
+
return {
|
|
4045
|
+
allowed: false,
|
|
4046
|
+
revision: options._rev ?? "",
|
|
4047
|
+
error: { code: failureCode, message: "Failed to read content revision" },
|
|
4048
|
+
};
|
|
4049
|
+
}
|
|
4050
|
+
if (options._rev !== undefined && options._rev !== revision) {
|
|
4051
|
+
return {
|
|
4052
|
+
allowed: false,
|
|
4053
|
+
revision,
|
|
4054
|
+
error: { code: "CONFLICT", message: "Revision precondition did not match" },
|
|
4055
|
+
};
|
|
4056
|
+
}
|
|
4057
|
+
|
|
4058
|
+
if (!this.hooks.hasHooks(name)) return { allowed: true, revision };
|
|
4059
|
+
if (name !== "content:beforeUnpublish") {
|
|
4060
|
+
try {
|
|
4061
|
+
current = await this.hydrateDraftData(current, {
|
|
4062
|
+
includeStagedSlug: true,
|
|
4063
|
+
strict: true,
|
|
4064
|
+
});
|
|
4065
|
+
} catch (error) {
|
|
4066
|
+
console.error(`[content-policy] ${name} draft hydration failed:`, error);
|
|
4067
|
+
return {
|
|
4068
|
+
allowed: false,
|
|
4069
|
+
revision,
|
|
4070
|
+
error: { code: failureCode, message: `Failed to read content for ${name} policy` },
|
|
4071
|
+
};
|
|
4072
|
+
}
|
|
4073
|
+
}
|
|
4074
|
+
|
|
4075
|
+
const origin = options.origin ?? { source: "system" };
|
|
4076
|
+
const actor =
|
|
4077
|
+
options.actor &&
|
|
4078
|
+
(origin.source === "api" || origin.source === "mcp" || origin.source === "visual-editor")
|
|
4079
|
+
? { ...options.actor, source: origin.source }
|
|
4080
|
+
: undefined;
|
|
4081
|
+
try {
|
|
4082
|
+
const decision = await this.hooks.runContentPolicy(name, {
|
|
4083
|
+
content: contentItemToRecord(current.data.item),
|
|
4084
|
+
collection,
|
|
4085
|
+
origin,
|
|
4086
|
+
actor,
|
|
4087
|
+
...(scheduledAt === undefined ? {} : { scheduledAt }),
|
|
4088
|
+
});
|
|
4089
|
+
if (decision.cancellation) {
|
|
4090
|
+
return {
|
|
4091
|
+
allowed: false,
|
|
4092
|
+
revision,
|
|
4093
|
+
error: { code: rejectionCode, message: decision.cancellation.reason },
|
|
4094
|
+
cancellation: decision.cancellation,
|
|
4095
|
+
};
|
|
4096
|
+
}
|
|
4097
|
+
return { allowed: true, revision };
|
|
4098
|
+
} catch (error) {
|
|
4099
|
+
console.error(`[content-policy] ${name} failed:`, error);
|
|
4100
|
+
return {
|
|
4101
|
+
allowed: false,
|
|
4102
|
+
revision,
|
|
4103
|
+
error: { code: failureCode, message: `Failed to run ${name} policy` },
|
|
4104
|
+
};
|
|
4105
|
+
}
|
|
4106
|
+
}
|
|
4107
|
+
|
|
4108
|
+
private async getScheduledPolicyRejectionRevision(
|
|
4109
|
+
collection: string,
|
|
4110
|
+
id: string,
|
|
4111
|
+
): Promise<string | undefined> {
|
|
4112
|
+
try {
|
|
4113
|
+
return (
|
|
4114
|
+
await new OptionsRepository(this.db).getVersioned(
|
|
4115
|
+
scheduledPolicyRejectionKey(collection, id),
|
|
4116
|
+
)
|
|
4117
|
+
)?.revision;
|
|
4118
|
+
} catch (error) {
|
|
4119
|
+
console.error(
|
|
4120
|
+
`[content-policy] Failed to read scheduled rejection for ${collection}/${id}:`,
|
|
4121
|
+
error,
|
|
4122
|
+
);
|
|
4123
|
+
return undefined;
|
|
4124
|
+
}
|
|
4125
|
+
}
|
|
4126
|
+
|
|
4127
|
+
private async clearScheduledPolicyRejection(
|
|
4128
|
+
collection: string,
|
|
4129
|
+
id: string,
|
|
4130
|
+
expectedRevision: string | undefined,
|
|
4131
|
+
): Promise<void> {
|
|
4132
|
+
if (expectedRevision === undefined) return;
|
|
4133
|
+
try {
|
|
4134
|
+
await new OptionsRepository(this.db).compareAndDelete(
|
|
4135
|
+
scheduledPolicyRejectionKey(collection, id),
|
|
4136
|
+
expectedRevision,
|
|
4137
|
+
);
|
|
4138
|
+
} catch (error) {
|
|
4139
|
+
console.error(
|
|
4140
|
+
`[content-policy] Failed to clear scheduled rejection for ${collection}/${id}:`,
|
|
4141
|
+
error,
|
|
4142
|
+
);
|
|
4143
|
+
}
|
|
4144
|
+
}
|
|
4145
|
+
|
|
4146
|
+
private createScheduledPolicyRejection(
|
|
4147
|
+
collection: string,
|
|
4148
|
+
id: string,
|
|
4149
|
+
cancellation: { pluginId: string; reason: string },
|
|
4150
|
+
): ScheduledPolicyRejection {
|
|
4151
|
+
return {
|
|
4152
|
+
collection,
|
|
4153
|
+
id,
|
|
4154
|
+
pluginId: cancellation.pluginId,
|
|
4155
|
+
reason: cancellation.reason,
|
|
4156
|
+
rejectedAt: (this.runtimeDeps.now?.() ?? new Date()).toISOString(),
|
|
4157
|
+
};
|
|
4158
|
+
}
|
|
4159
|
+
|
|
4160
|
+
async handleContentPublish(
|
|
4161
|
+
collection: string,
|
|
4162
|
+
id: string,
|
|
4163
|
+
options: {
|
|
4164
|
+
publishedAt?: string;
|
|
4165
|
+
requireScheduledDue?: boolean;
|
|
4166
|
+
expectedScheduledAt?: string;
|
|
4167
|
+
_rev?: string;
|
|
4168
|
+
currentTime?: Date;
|
|
4169
|
+
actor?: ActorInfo;
|
|
4170
|
+
origin?: ContentActionOrigin;
|
|
4171
|
+
pluginInvocationId?: string;
|
|
4172
|
+
} = {},
|
|
4173
|
+
) {
|
|
4174
|
+
const policy = await this.checkContentPolicy(
|
|
4175
|
+
"content:beforePublish",
|
|
4176
|
+
collection,
|
|
4177
|
+
id,
|
|
4178
|
+
options,
|
|
4179
|
+
"PUBLISH_REJECTED",
|
|
4180
|
+
"CONTENT_PUBLISH_ERROR",
|
|
4181
|
+
);
|
|
4182
|
+
if (!policy.allowed) {
|
|
4183
|
+
if (policy.cancellation && options.origin?.source === "scheduler") {
|
|
4184
|
+
const unscheduled = await handleScheduledPolicyRejection(this.db, collection, id, {
|
|
4185
|
+
_rev: policy.revision,
|
|
4186
|
+
rejection: this.createScheduledPolicyRejection(collection, id, policy.cancellation),
|
|
4187
|
+
});
|
|
4188
|
+
if (!unscheduled.success) return unscheduled;
|
|
4189
|
+
await this.refreshContentUsageAfterSuccessfulWrite(collection, [unscheduled.data.item.id]);
|
|
4190
|
+
this.runAfterUnscheduleHooks(contentItemToRecord(unscheduled.data.item), collection);
|
|
4191
|
+
}
|
|
4192
|
+
return { success: false as const, error: policy.error };
|
|
4193
|
+
}
|
|
4194
|
+
const policyRejectionRevision = await this.getScheduledPolicyRejectionRevision(collection, id);
|
|
4195
|
+
const {
|
|
4196
|
+
actor: _actor,
|
|
4197
|
+
origin: _origin,
|
|
4198
|
+
pluginInvocationId: _pluginInvocationId,
|
|
4199
|
+
...handlerOptions
|
|
4200
|
+
} = options;
|
|
4201
|
+
const result = await handleContentPublish(this.db, collection, id, {
|
|
4202
|
+
...handlerOptions,
|
|
4203
|
+
_rev: policy.revision,
|
|
4204
|
+
});
|
|
4205
|
+
if (result.success && result.data) {
|
|
4206
|
+
await this.clearScheduledPolicyRejection(
|
|
4207
|
+
collection,
|
|
4208
|
+
result.data.item.id,
|
|
4209
|
+
policyRejectionRevision,
|
|
4210
|
+
);
|
|
4211
|
+
const { item } = result.data;
|
|
4212
|
+
await this.refreshContentUsageAfterSuccessfulWrite(collection, [
|
|
4213
|
+
item.id,
|
|
4214
|
+
...(await this.findSyncedSiblingsForUsageRefresh(
|
|
4215
|
+
collection,
|
|
4216
|
+
item.id,
|
|
4217
|
+
item.translationGroup,
|
|
4218
|
+
item.data,
|
|
4219
|
+
{ absentAsCleared: true },
|
|
4220
|
+
)),
|
|
4221
|
+
]);
|
|
4222
|
+
}
|
|
4223
|
+
|
|
4224
|
+
// Run afterPublish hooks (fire-and-forget)
|
|
4225
|
+
if (result.success && result.data) {
|
|
4226
|
+
this.runAfterPublishHooks(
|
|
4227
|
+
contentItemToRecord(result.data.item),
|
|
4228
|
+
collection,
|
|
4229
|
+
options.origin?.source === "plugin" ? options.origin.pluginId : undefined,
|
|
4230
|
+
options.pluginInvocationId,
|
|
4231
|
+
);
|
|
4232
|
+
}
|
|
4233
|
+
|
|
4234
|
+
return result;
|
|
4235
|
+
}
|
|
4236
|
+
|
|
4237
|
+
async handleContentUnpublish(
|
|
4238
|
+
collection: string,
|
|
4239
|
+
id: string,
|
|
4240
|
+
options: ContentPolicyMutationOptions = {},
|
|
4241
|
+
) {
|
|
4242
|
+
const policy = await this.checkContentPolicy(
|
|
4243
|
+
"content:beforeUnpublish",
|
|
4244
|
+
collection,
|
|
4245
|
+
id,
|
|
4246
|
+
options,
|
|
4247
|
+
"UNPUBLISH_REJECTED",
|
|
4248
|
+
"CONTENT_UNPUBLISH_ERROR",
|
|
4249
|
+
);
|
|
4250
|
+
if (!policy.allowed) return { success: false as const, error: policy.error };
|
|
4251
|
+
const result = await handleContentUnpublish(this.db, collection, id, {
|
|
4252
|
+
_rev: policy.revision,
|
|
4253
|
+
});
|
|
3240
4254
|
if (result.success && result.data) {
|
|
3241
4255
|
await this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.item.id]);
|
|
3242
4256
|
}
|
|
3243
4257
|
|
|
3244
4258
|
// Run afterUnpublish hooks (deferred past the response via after())
|
|
3245
4259
|
if (result.success && result.data) {
|
|
3246
|
-
this.runAfterUnpublishHooks(
|
|
4260
|
+
this.runAfterUnpublishHooks(
|
|
4261
|
+
contentItemToRecord(result.data.item),
|
|
4262
|
+
collection,
|
|
4263
|
+
options.origin?.source === "plugin" ? options.origin.pluginId : undefined,
|
|
4264
|
+
options.pluginInvocationId,
|
|
4265
|
+
);
|
|
3247
4266
|
}
|
|
3248
4267
|
|
|
3249
4268
|
return result;
|
|
3250
4269
|
}
|
|
3251
4270
|
|
|
3252
|
-
async handleContentSchedule(
|
|
3253
|
-
|
|
4271
|
+
async handleContentSchedule(
|
|
4272
|
+
collection: string,
|
|
4273
|
+
id: string,
|
|
4274
|
+
scheduledAt: string,
|
|
4275
|
+
options: ContentPolicyMutationOptions = {},
|
|
4276
|
+
) {
|
|
4277
|
+
const policy = await this.checkContentPolicy(
|
|
4278
|
+
"content:beforeSchedule",
|
|
4279
|
+
collection,
|
|
4280
|
+
id,
|
|
4281
|
+
options,
|
|
4282
|
+
"SCHEDULE_REJECTED",
|
|
4283
|
+
"CONTENT_SCHEDULE_ERROR",
|
|
4284
|
+
scheduledAt,
|
|
4285
|
+
);
|
|
4286
|
+
if (!policy.allowed) return { success: false as const, error: policy.error };
|
|
4287
|
+
const policyRejectionRevision = await this.getScheduledPolicyRejectionRevision(collection, id);
|
|
4288
|
+
const result = await handleContentSchedule(
|
|
4289
|
+
this.db,
|
|
4290
|
+
collection,
|
|
4291
|
+
id,
|
|
4292
|
+
scheduledAt,
|
|
4293
|
+
this.runtimeDeps.now?.() ?? new Date(),
|
|
4294
|
+
policy.revision,
|
|
4295
|
+
);
|
|
3254
4296
|
if (result.success && result.data) {
|
|
4297
|
+
await this.clearScheduledPolicyRejection(
|
|
4298
|
+
collection,
|
|
4299
|
+
result.data.item.id,
|
|
4300
|
+
policyRejectionRevision,
|
|
4301
|
+
);
|
|
3255
4302
|
await this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.item.id]);
|
|
3256
4303
|
}
|
|
3257
4304
|
|
|
3258
4305
|
// Run afterSchedule hooks (fire-and-forget)
|
|
3259
4306
|
if (result.success && result.data) {
|
|
3260
|
-
this.runAfterScheduleHooks(
|
|
4307
|
+
this.runAfterScheduleHooks(
|
|
4308
|
+
contentItemToRecord(result.data.item),
|
|
4309
|
+
collection,
|
|
4310
|
+
options.origin?.source === "plugin" ? options.origin.pluginId : undefined,
|
|
4311
|
+
options.pluginInvocationId,
|
|
4312
|
+
);
|
|
3261
4313
|
}
|
|
3262
4314
|
|
|
3263
4315
|
return result;
|
|
3264
4316
|
}
|
|
3265
4317
|
|
|
3266
|
-
async handleContentUnschedule(
|
|
3267
|
-
|
|
4318
|
+
async handleContentUnschedule(
|
|
4319
|
+
collection: string,
|
|
4320
|
+
id: string,
|
|
4321
|
+
options: {
|
|
4322
|
+
_rev?: string;
|
|
4323
|
+
origin?: ContentActionOrigin;
|
|
4324
|
+
pluginInvocationId?: string;
|
|
4325
|
+
} = {},
|
|
4326
|
+
) {
|
|
4327
|
+
const result = await handleContentUnschedule(this.db, collection, id, { _rev: options._rev });
|
|
3268
4328
|
if (result.success && result.data) {
|
|
3269
4329
|
await this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.item.id]);
|
|
3270
4330
|
}
|
|
3271
4331
|
|
|
3272
4332
|
// Run afterUnschedule hooks (fire-and-forget)
|
|
3273
4333
|
if (result.success && result.data) {
|
|
3274
|
-
this.runAfterUnscheduleHooks(
|
|
4334
|
+
this.runAfterUnscheduleHooks(
|
|
4335
|
+
contentItemToRecord(result.data.item),
|
|
4336
|
+
collection,
|
|
4337
|
+
options.origin?.source === "plugin" ? options.origin.pluginId : undefined,
|
|
4338
|
+
options.pluginInvocationId,
|
|
4339
|
+
);
|
|
3275
4340
|
}
|
|
3276
4341
|
|
|
3277
4342
|
return result;
|
|
@@ -3316,6 +4381,33 @@ export class EmDashRuntime {
|
|
|
3316
4381
|
return handleMediaGet(this.db, id);
|
|
3317
4382
|
}
|
|
3318
4383
|
|
|
4384
|
+
async handleMediaUpload(input: MediaUploadInput) {
|
|
4385
|
+
if (!this.storage) {
|
|
4386
|
+
return {
|
|
4387
|
+
success: false as const,
|
|
4388
|
+
error: { code: "NO_STORAGE", message: "Storage not configured" },
|
|
4389
|
+
};
|
|
4390
|
+
}
|
|
4391
|
+
const result = await uploadMedia(this.db, this.storage, input, {
|
|
4392
|
+
beforeUpload: async (file) => {
|
|
4393
|
+
if (!this.hooks.hasHooks("media:beforeUpload")) return file;
|
|
4394
|
+
return (await this.hooks.runMediaBeforeUpload(file)).file;
|
|
4395
|
+
},
|
|
4396
|
+
});
|
|
4397
|
+
if (result.success && !result.data.deduplicated && this.hooks.hasHooks("media:afterUpload")) {
|
|
4398
|
+
const item = result.data.item;
|
|
4399
|
+
await this.hooks.runMediaAfterUpload({
|
|
4400
|
+
id: item.id,
|
|
4401
|
+
filename: item.filename,
|
|
4402
|
+
mimeType: item.mimeType,
|
|
4403
|
+
size: item.size,
|
|
4404
|
+
url: item.url,
|
|
4405
|
+
createdAt: item.createdAt,
|
|
4406
|
+
});
|
|
4407
|
+
}
|
|
4408
|
+
return result;
|
|
4409
|
+
}
|
|
4410
|
+
|
|
3319
4411
|
async handleMediaCreate(input: {
|
|
3320
4412
|
filename: string;
|
|
3321
4413
|
mimeType: string;
|
|
@@ -3392,6 +4484,10 @@ export class EmDashRuntime {
|
|
|
3392
4484
|
return result;
|
|
3393
4485
|
}
|
|
3394
4486
|
|
|
4487
|
+
async handlePluginMediaMetadataUpdate(id: string, patch: MediaMetadataPatch) {
|
|
4488
|
+
return updatePluginMediaMetadata(this.db, id, patch);
|
|
4489
|
+
}
|
|
4490
|
+
|
|
3395
4491
|
async handleMediaReplaceMetadata(
|
|
3396
4492
|
id: string,
|
|
3397
4493
|
expectedStorageKey: string,
|
|
@@ -3416,6 +4512,163 @@ export class EmDashRuntime {
|
|
|
3416
4512
|
return result;
|
|
3417
4513
|
}
|
|
3418
4514
|
|
|
4515
|
+
private commentHooks(): CommentHookRunner {
|
|
4516
|
+
return {
|
|
4517
|
+
runBeforeCreate: (event) => this.hooks.runCommentBeforeCreate(event),
|
|
4518
|
+
runModerate: async (event) => {
|
|
4519
|
+
const result = await this.hooks.invokeExclusiveHook("comment:moderate", event);
|
|
4520
|
+
if (!result) return { status: "pending", reason: "No moderator configured" };
|
|
4521
|
+
if (result.error) {
|
|
4522
|
+
console.error(`[comments] Moderation error (${result.pluginId}):`, result.error.message);
|
|
4523
|
+
return { status: "pending", reason: "Moderation error" };
|
|
4524
|
+
}
|
|
4525
|
+
if (!isModerationDecision(result.result)) {
|
|
4526
|
+
return { status: "pending", reason: "Invalid moderation result" };
|
|
4527
|
+
}
|
|
4528
|
+
return result.result;
|
|
4529
|
+
},
|
|
4530
|
+
fireAfterCreate: (event) => {
|
|
4531
|
+
void this.hooks
|
|
4532
|
+
.runCommentAfterCreate(event)
|
|
4533
|
+
.catch((error) =>
|
|
4534
|
+
console.error(
|
|
4535
|
+
"[comments] afterCreate error:",
|
|
4536
|
+
error instanceof Error ? error.message : error,
|
|
4537
|
+
),
|
|
4538
|
+
);
|
|
4539
|
+
},
|
|
4540
|
+
fireAfterModerate: (event) => {
|
|
4541
|
+
return this.hooks
|
|
4542
|
+
.runCommentAfterModerate(event)
|
|
4543
|
+
.catch((error) =>
|
|
4544
|
+
console.error(
|
|
4545
|
+
"[comments] afterModerate error:",
|
|
4546
|
+
error instanceof Error ? error.message : error,
|
|
4547
|
+
),
|
|
4548
|
+
);
|
|
4549
|
+
},
|
|
4550
|
+
};
|
|
4551
|
+
}
|
|
4552
|
+
|
|
4553
|
+
async handleCommentCreate(
|
|
4554
|
+
input: CommentCreateInput,
|
|
4555
|
+
settings: CollectionCommentSettings,
|
|
4556
|
+
contentInfo?: Parameters<typeof createComment>[4],
|
|
4557
|
+
): Promise<CommentCreateResult | null> {
|
|
4558
|
+
return createComment(this.db, input, settings, this.commentHooks(), contentInfo);
|
|
4559
|
+
}
|
|
4560
|
+
|
|
4561
|
+
async handlePublicCommentSubmission(
|
|
4562
|
+
collection: string,
|
|
4563
|
+
contentId: string,
|
|
4564
|
+
request: Request,
|
|
4565
|
+
user?: UserInfo,
|
|
4566
|
+
): Promise<Response> {
|
|
4567
|
+
return submitPublicComment(this, collection, contentId, request, user);
|
|
4568
|
+
}
|
|
4569
|
+
|
|
4570
|
+
private async sendCommentApproval(
|
|
4571
|
+
comment: Awaited<ReturnType<CommentRepository["findById"]>>,
|
|
4572
|
+
request?: Request,
|
|
4573
|
+
) {
|
|
4574
|
+
if (!comment || !this.email) return;
|
|
4575
|
+
try {
|
|
4576
|
+
const adminBaseUrl = await getSiteBaseUrl(
|
|
4577
|
+
this.db,
|
|
4578
|
+
request ?? new Request("https://plugin.invalid/"),
|
|
4579
|
+
this.config,
|
|
4580
|
+
);
|
|
4581
|
+
const content = await lookupContentAuthor(this.db, comment.collection, comment.contentId);
|
|
4582
|
+
if (content?.author) {
|
|
4583
|
+
await sendCommentNotification({
|
|
4584
|
+
email: this.email,
|
|
4585
|
+
comment,
|
|
4586
|
+
contentAuthor: content.author,
|
|
4587
|
+
adminBaseUrl,
|
|
4588
|
+
});
|
|
4589
|
+
}
|
|
4590
|
+
} catch (error) {
|
|
4591
|
+
console.error(
|
|
4592
|
+
"[comments] notification error:",
|
|
4593
|
+
error instanceof Error ? error.message : error,
|
|
4594
|
+
);
|
|
4595
|
+
}
|
|
4596
|
+
}
|
|
4597
|
+
|
|
4598
|
+
private async moderateCommentWithOrigin(
|
|
4599
|
+
id: string,
|
|
4600
|
+
status: CommentStatus,
|
|
4601
|
+
expectedStatus: CommentStatus,
|
|
4602
|
+
origin:
|
|
4603
|
+
| { source: "admin"; userId: string; name: string | null }
|
|
4604
|
+
| { source: "plugin"; pluginId: string },
|
|
4605
|
+
request?: Request,
|
|
4606
|
+
) {
|
|
4607
|
+
if (this.commentModerationInProgress.has(id)) {
|
|
4608
|
+
const current = await new CommentRepository(this.db).findById(id);
|
|
4609
|
+
if (current && current.status !== expectedStatus) {
|
|
4610
|
+
throw new CommentStatusConflictError(current.status);
|
|
4611
|
+
}
|
|
4612
|
+
throw Object.assign(new Error("Comment moderation is already in progress"), {
|
|
4613
|
+
code: "COMMENT_MODERATION_IN_PROGRESS",
|
|
4614
|
+
});
|
|
4615
|
+
}
|
|
4616
|
+
this.commentModerationInProgress.add(id);
|
|
4617
|
+
try {
|
|
4618
|
+
return await moderateComment(
|
|
4619
|
+
this.db,
|
|
4620
|
+
id,
|
|
4621
|
+
status,
|
|
4622
|
+
expectedStatus,
|
|
4623
|
+
origin,
|
|
4624
|
+
this.commentHooks(),
|
|
4625
|
+
(comment) => this.sendCommentApproval(comment, request),
|
|
4626
|
+
);
|
|
4627
|
+
} finally {
|
|
4628
|
+
this.commentModerationInProgress.delete(id);
|
|
4629
|
+
}
|
|
4630
|
+
}
|
|
4631
|
+
|
|
4632
|
+
async handleCommentModerate(
|
|
4633
|
+
id: string,
|
|
4634
|
+
status: CommentStatus,
|
|
4635
|
+
moderator: { id: string; name: string | null },
|
|
4636
|
+
expectedStatus?: CommentStatus,
|
|
4637
|
+
request?: Request,
|
|
4638
|
+
) {
|
|
4639
|
+
let expected = expectedStatus;
|
|
4640
|
+
if (!expected) {
|
|
4641
|
+
const current = await new CommentRepository(this.db).findById(id);
|
|
4642
|
+
if (!current || current.status === "trash") return null;
|
|
4643
|
+
expected = current.status;
|
|
4644
|
+
}
|
|
4645
|
+
return this.moderateCommentWithOrigin(
|
|
4646
|
+
id,
|
|
4647
|
+
status,
|
|
4648
|
+
expected,
|
|
4649
|
+
{ source: "admin", userId: moderator.id, name: moderator.name },
|
|
4650
|
+
request,
|
|
4651
|
+
);
|
|
4652
|
+
}
|
|
4653
|
+
|
|
4654
|
+
async handlePluginCommentModerate(
|
|
4655
|
+
pluginId: string,
|
|
4656
|
+
id: string,
|
|
4657
|
+
status: PluginCommentStatus,
|
|
4658
|
+
expectedStatus: PluginCommentStatus,
|
|
4659
|
+
): Promise<PluginComment> {
|
|
4660
|
+
assertPluginCommentStatus(status, "status");
|
|
4661
|
+
assertPluginCommentStatus(expectedStatus, "expectedStatus");
|
|
4662
|
+
const updated = await this.moderateCommentWithOrigin(id, status, expectedStatus, {
|
|
4663
|
+
source: "plugin",
|
|
4664
|
+
pluginId,
|
|
4665
|
+
});
|
|
4666
|
+
if (!updated) throw new Error(`Comment not found: ${id}`);
|
|
4667
|
+
const result = await createCommentAccess(this.db).get(id);
|
|
4668
|
+
if (!result) throw new Error(`Comment not found: ${id}`);
|
|
4669
|
+
return result;
|
|
4670
|
+
}
|
|
4671
|
+
|
|
3419
4672
|
// =========================================================================
|
|
3420
4673
|
// Revision Handlers
|
|
3421
4674
|
// =========================================================================
|
|
@@ -3467,8 +4720,13 @@ export class EmDashRuntime {
|
|
|
3467
4720
|
// live, matching the documented tool contract.
|
|
3468
4721
|
try {
|
|
3469
4722
|
const contentRepo = new ContentRepository(this.db);
|
|
3470
|
-
const
|
|
3471
|
-
|
|
4723
|
+
const newDraftId = await contentRepo.restoreDraftRevision(
|
|
4724
|
+
revision.collection,
|
|
4725
|
+
revision.entryId,
|
|
4726
|
+
revision.data,
|
|
4727
|
+
callerUserId,
|
|
4728
|
+
);
|
|
4729
|
+
if (!newDraftId) {
|
|
3472
4730
|
return {
|
|
3473
4731
|
success: false as const,
|
|
3474
4732
|
error: {
|
|
@@ -3478,43 +4736,12 @@ export class EmDashRuntime {
|
|
|
3478
4736
|
};
|
|
3479
4737
|
}
|
|
3480
4738
|
|
|
3481
|
-
const newDraft = await revisionRepo.create({
|
|
3482
|
-
collection: revision.collection,
|
|
3483
|
-
entryId: revision.entryId,
|
|
3484
|
-
data: revision.data,
|
|
3485
|
-
authorId: callerUserId,
|
|
3486
|
-
});
|
|
3487
|
-
|
|
3488
|
-
try {
|
|
3489
|
-
const staged = await contentRepo.replaceDraftRevision(
|
|
3490
|
-
revision.collection,
|
|
3491
|
-
revision.entryId,
|
|
3492
|
-
newDraft.id,
|
|
3493
|
-
existing,
|
|
3494
|
-
);
|
|
3495
|
-
if (!staged) throw new ContentMutationConflictError();
|
|
3496
|
-
} catch (error) {
|
|
3497
|
-
try {
|
|
3498
|
-
await revisionRepo.deleteIfUnreferenced(
|
|
3499
|
-
revision.collection,
|
|
3500
|
-
revision.entryId,
|
|
3501
|
-
newDraft.id,
|
|
3502
|
-
);
|
|
3503
|
-
} catch (cleanupError) {
|
|
3504
|
-
console.error(
|
|
3505
|
-
`[emdash] Failed to clean up unrestored revision ${newDraft.id}:`,
|
|
3506
|
-
cleanupError,
|
|
3507
|
-
);
|
|
3508
|
-
}
|
|
3509
|
-
throw error;
|
|
3510
|
-
}
|
|
3511
|
-
|
|
3512
4739
|
after(async () => {
|
|
3513
4740
|
try {
|
|
3514
4741
|
await revisionRepo.pruneQueuedEntry(
|
|
3515
4742
|
revision.collection,
|
|
3516
4743
|
revision.entryId,
|
|
3517
|
-
|
|
4744
|
+
newDraftId,
|
|
3518
4745
|
50,
|
|
3519
4746
|
);
|
|
3520
4747
|
} catch (error) {
|
|
@@ -3553,6 +4780,40 @@ export class EmDashRuntime {
|
|
|
3553
4780
|
}
|
|
3554
4781
|
}
|
|
3555
4782
|
|
|
4783
|
+
private async findSyncedSiblingsForUsageRefresh(
|
|
4784
|
+
collection: string,
|
|
4785
|
+
contentId: string,
|
|
4786
|
+
translationGroup: string | null | undefined,
|
|
4787
|
+
data: Record<string, unknown>,
|
|
4788
|
+
options: { absentAsCleared?: boolean } = {},
|
|
4789
|
+
): Promise<string[]> {
|
|
4790
|
+
try {
|
|
4791
|
+
return await findNonTranslatableSiblingContentIds(
|
|
4792
|
+
this.db,
|
|
4793
|
+
collection,
|
|
4794
|
+
contentId,
|
|
4795
|
+
translationGroup,
|
|
4796
|
+
data,
|
|
4797
|
+
options,
|
|
4798
|
+
);
|
|
4799
|
+
} catch (error) {
|
|
4800
|
+
console.error(
|
|
4801
|
+
`[media-usage] Failed to discover synced i18n siblings for ${collection}/${contentId}:`,
|
|
4802
|
+
error,
|
|
4803
|
+
);
|
|
4804
|
+
try {
|
|
4805
|
+
await markContentMediaUsageCollectionStale(
|
|
4806
|
+
this.db,
|
|
4807
|
+
collection,
|
|
4808
|
+
"CONTENT_USAGE_REFRESH_ERROR",
|
|
4809
|
+
);
|
|
4810
|
+
} catch (staleError) {
|
|
4811
|
+
console.error(`[media-usage] Failed to mark ${collection} stale:`, staleError);
|
|
4812
|
+
}
|
|
4813
|
+
return [];
|
|
4814
|
+
}
|
|
4815
|
+
}
|
|
4816
|
+
|
|
3556
4817
|
private async refreshContentUsageAfterSuccessfulWrite(
|
|
3557
4818
|
collection: string,
|
|
3558
4819
|
contentIds: readonly string[],
|
|
@@ -3597,6 +4858,211 @@ export class EmDashRuntime {
|
|
|
3597
4858
|
// Plugin Routes
|
|
3598
4859
|
// =========================================================================
|
|
3599
4860
|
|
|
4861
|
+
private getSandboxedAdminDefinition(pluginId: string): {
|
|
4862
|
+
pages: string[];
|
|
4863
|
+
widgets: string[];
|
|
4864
|
+
policy: BlockValidationPolicy;
|
|
4865
|
+
} | null {
|
|
4866
|
+
const entry = this.sandboxedPluginEntries.find((candidate) => candidate.id === pluginId);
|
|
4867
|
+
if (entry) {
|
|
4868
|
+
const pages = (entry.adminPages ?? []).map((page) => normalizePluginPagePath(page.path));
|
|
4869
|
+
const imageHosts = allowedBrowserImageHosts(entry.capabilities, entry.allowedHosts);
|
|
4870
|
+
return {
|
|
4871
|
+
pages,
|
|
4872
|
+
widgets: (entry.adminWidgets ?? []).map((widget) => widget.id),
|
|
4873
|
+
policy: { pluginPagePaths: pages, allowedImageHosts: imageHosts },
|
|
4874
|
+
};
|
|
4875
|
+
}
|
|
4876
|
+
|
|
4877
|
+
const manifest = marketplaceManifestCache.get(pluginId);
|
|
4878
|
+
if (!manifest) return null;
|
|
4879
|
+
const pages = (manifest.admin?.pages ?? []).map((page) => normalizePluginPagePath(page.path));
|
|
4880
|
+
const imageHosts = allowedBrowserImageHosts(
|
|
4881
|
+
manifest.capabilities ?? [],
|
|
4882
|
+
manifest.allowedHosts ?? [],
|
|
4883
|
+
);
|
|
4884
|
+
return {
|
|
4885
|
+
pages,
|
|
4886
|
+
widgets: (manifest.admin?.widgets ?? []).map((widget) => widget.id),
|
|
4887
|
+
policy: { pluginPagePaths: pages, allowedImageHosts: imageHosts },
|
|
4888
|
+
};
|
|
4889
|
+
}
|
|
4890
|
+
|
|
4891
|
+
private resolvePluginUiContext(
|
|
4892
|
+
definition: { pages: string[]; widgets: string[] },
|
|
4893
|
+
body: unknown,
|
|
4894
|
+
request: Request,
|
|
4895
|
+
): { context?: PluginUiContext; error?: { code: string; message: string } } {
|
|
4896
|
+
if (typeof body !== "object" || body === null || !("page" in body)) return {};
|
|
4897
|
+
const page = body.page;
|
|
4898
|
+
if (typeof page !== "string") {
|
|
4899
|
+
return {
|
|
4900
|
+
error: { code: "INVALID_PLUGIN_UI_CONTEXT", message: "Plugin UI page must be a string" },
|
|
4901
|
+
};
|
|
4902
|
+
}
|
|
4903
|
+
|
|
4904
|
+
let surface: PluginUiContext["surface"];
|
|
4905
|
+
if (page.startsWith("widget:")) {
|
|
4906
|
+
if (!definition.widgets.includes(page.slice("widget:".length))) {
|
|
4907
|
+
return {
|
|
4908
|
+
error: {
|
|
4909
|
+
code: "INVALID_PLUGIN_UI_CONTEXT",
|
|
4910
|
+
message: "Plugin dashboard widget is not declared",
|
|
4911
|
+
},
|
|
4912
|
+
};
|
|
4913
|
+
}
|
|
4914
|
+
surface = "dashboard-widget";
|
|
4915
|
+
} else {
|
|
4916
|
+
if (!definition.pages.includes(normalizePluginPagePath(page))) {
|
|
4917
|
+
return {
|
|
4918
|
+
error: {
|
|
4919
|
+
code: "INVALID_PLUGIN_UI_CONTEXT",
|
|
4920
|
+
message: "Plugin admin page is not declared",
|
|
4921
|
+
},
|
|
4922
|
+
};
|
|
4923
|
+
}
|
|
4924
|
+
surface = "admin-page";
|
|
4925
|
+
}
|
|
4926
|
+
|
|
4927
|
+
const locale = resolveLocale(request);
|
|
4928
|
+
return { context: { surface, locale, direction: getLocaleDir(locale) } };
|
|
4929
|
+
}
|
|
4930
|
+
|
|
4931
|
+
private validateSandboxedAdminResponse(
|
|
4932
|
+
pluginId: string,
|
|
4933
|
+
definition: { policy: BlockValidationPolicy },
|
|
4934
|
+
result: {
|
|
4935
|
+
success: boolean;
|
|
4936
|
+
data?: unknown;
|
|
4937
|
+
error?: { code: string; message: string };
|
|
4938
|
+
status?: number;
|
|
4939
|
+
},
|
|
4940
|
+
) {
|
|
4941
|
+
if (!result.success) return result;
|
|
4942
|
+
const validation = validateBlockResponse(result.data, definition.policy);
|
|
4943
|
+
if (validation.valid) return result;
|
|
4944
|
+
|
|
4945
|
+
console.error(
|
|
4946
|
+
`EmDash: Sandboxed plugin ${pluginId} returned invalid Block Kit content:`,
|
|
4947
|
+
validation.errors,
|
|
4948
|
+
);
|
|
4949
|
+
return {
|
|
4950
|
+
success: false,
|
|
4951
|
+
status: 502,
|
|
4952
|
+
error: {
|
|
4953
|
+
code: "INVALID_BLOCK_RESPONSE",
|
|
4954
|
+
message: "Plugin returned invalid Block Kit content",
|
|
4955
|
+
},
|
|
4956
|
+
};
|
|
4957
|
+
}
|
|
4958
|
+
|
|
4959
|
+
getPluginEditorExtension(
|
|
4960
|
+
pluginId: string,
|
|
4961
|
+
kind: "panel" | "action",
|
|
4962
|
+
extensionId: string,
|
|
4963
|
+
collection: string,
|
|
4964
|
+
): ResolvedPluginEditorExtension | null {
|
|
4965
|
+
if (!this.isPluginEnabled(pluginId)) return null;
|
|
4966
|
+
|
|
4967
|
+
let panels: PluginEditorPanel[] | undefined;
|
|
4968
|
+
let actions: PluginEditorAction[] | undefined;
|
|
4969
|
+
let pages: string[] = [];
|
|
4970
|
+
let capabilities: readonly PluginCapability[] = [];
|
|
4971
|
+
let allowedHosts: readonly string[] = [];
|
|
4972
|
+
|
|
4973
|
+
const configured = this.configuredPlugins.find((plugin) => plugin.id === pluginId);
|
|
4974
|
+
if (configured) {
|
|
4975
|
+
if (configured.admin.entry) return null;
|
|
4976
|
+
panels = configured.admin.editorPanels;
|
|
4977
|
+
actions = configured.admin.editorActions;
|
|
4978
|
+
pages = (configured.admin.pages ?? []).map((page) => page.path);
|
|
4979
|
+
capabilities = configured.capabilities;
|
|
4980
|
+
allowedHosts = configured.allowedHosts;
|
|
4981
|
+
} else {
|
|
4982
|
+
const entry = this.sandboxedPluginEntries.find((candidate) => candidate.id === pluginId);
|
|
4983
|
+
if (entry) {
|
|
4984
|
+
panels = entry.editorPanels;
|
|
4985
|
+
actions = entry.editorActions;
|
|
4986
|
+
pages = (entry.adminPages ?? []).map((page) => page.path);
|
|
4987
|
+
capabilities = entry.capabilities;
|
|
4988
|
+
allowedHosts = entry.allowedHosts;
|
|
4989
|
+
} else {
|
|
4990
|
+
const manifest = marketplaceManifestCache.get(pluginId);
|
|
4991
|
+
if (!manifest) return null;
|
|
4992
|
+
panels = manifest.admin?.editorPanels;
|
|
4993
|
+
actions = manifest.admin?.editorActions;
|
|
4994
|
+
pages = (manifest.admin?.pages ?? []).map((page) => page.path);
|
|
4995
|
+
capabilities = manifest.capabilities ?? [];
|
|
4996
|
+
allowedHosts = manifest.allowedHosts ?? [];
|
|
4997
|
+
}
|
|
4998
|
+
}
|
|
4999
|
+
|
|
5000
|
+
const normalizedCapabilities = normalizePluginCapabilities(capabilities);
|
|
5001
|
+
const policy = {
|
|
5002
|
+
pluginPagePaths: pages,
|
|
5003
|
+
allowedImageHosts: allowedBrowserImageHosts(capabilities, allowedHosts),
|
|
5004
|
+
};
|
|
5005
|
+
if (kind === "panel") {
|
|
5006
|
+
const matches = panels?.filter((panel) => panel.id === extensionId) ?? [];
|
|
5007
|
+
const extension = matches[0];
|
|
5008
|
+
if (
|
|
5009
|
+
matches.length !== 1 ||
|
|
5010
|
+
!extension ||
|
|
5011
|
+
(extension.collections && !extension.collections.includes(collection))
|
|
5012
|
+
) {
|
|
5013
|
+
return null;
|
|
5014
|
+
}
|
|
5015
|
+
return { kind, extension, policy, capabilities: normalizedCapabilities };
|
|
5016
|
+
}
|
|
5017
|
+
const matches = actions?.filter((action) => action.id === extensionId) ?? [];
|
|
5018
|
+
const extension = matches[0];
|
|
5019
|
+
if (
|
|
5020
|
+
matches.length !== 1 ||
|
|
5021
|
+
!extension ||
|
|
5022
|
+
(extension.collections && !extension.collections.includes(collection)) ||
|
|
5023
|
+
(extension.style === "danger" && !extension.confirm)
|
|
5024
|
+
) {
|
|
5025
|
+
return null;
|
|
5026
|
+
}
|
|
5027
|
+
return { kind, extension, policy, capabilities: normalizedCapabilities };
|
|
5028
|
+
}
|
|
5029
|
+
|
|
5030
|
+
async getPluginEditorDraftSchema(collection: string) {
|
|
5031
|
+
return this.schemaRegistry.getCollectionWithFields(collection);
|
|
5032
|
+
}
|
|
5033
|
+
|
|
5034
|
+
private validatePluginEditorExtensionResponse(
|
|
5035
|
+
pluginId: string,
|
|
5036
|
+
dispatch: PluginEditorExtensionDispatch,
|
|
5037
|
+
result: {
|
|
5038
|
+
success: boolean;
|
|
5039
|
+
data?: unknown;
|
|
5040
|
+
error?: { code: string; message: string };
|
|
5041
|
+
status?: number;
|
|
5042
|
+
},
|
|
5043
|
+
) {
|
|
5044
|
+
if (!result.success) return result;
|
|
5045
|
+
const validation =
|
|
5046
|
+
dispatch.kind === "panel"
|
|
5047
|
+
? validateBlockResponse(result.data, dispatch.policy)
|
|
5048
|
+
: validateContentEditorActionResponse(result.data, dispatch.policy);
|
|
5049
|
+
if (validation.valid) return result;
|
|
5050
|
+
|
|
5051
|
+
console.error(
|
|
5052
|
+
`EmDash: Plugin ${pluginId} returned an invalid content editor ${dispatch.kind} response:`,
|
|
5053
|
+
validation.errors,
|
|
5054
|
+
);
|
|
5055
|
+
return {
|
|
5056
|
+
success: false,
|
|
5057
|
+
status: 502,
|
|
5058
|
+
error: {
|
|
5059
|
+
code:
|
|
5060
|
+
dispatch.kind === "panel" ? "INVALID_BLOCK_RESPONSE" : "INVALID_EDITOR_ACTION_RESPONSE",
|
|
5061
|
+
message: "Plugin returned an invalid editor extension response",
|
|
5062
|
+
},
|
|
5063
|
+
};
|
|
5064
|
+
}
|
|
5065
|
+
|
|
3600
5066
|
/**
|
|
3601
5067
|
* Get route metadata for a plugin route without invoking the handler.
|
|
3602
5068
|
* Used by the catch-all route to decide auth before dispatch.
|
|
@@ -3660,10 +5126,12 @@ export class EmDashRuntime {
|
|
|
3660
5126
|
|
|
3661
5127
|
async handlePluginApiRoute(
|
|
3662
5128
|
pluginId: string,
|
|
3663
|
-
|
|
5129
|
+
method: string,
|
|
3664
5130
|
path: string,
|
|
3665
5131
|
request: Request,
|
|
3666
5132
|
user?: RouteCallerInput | null,
|
|
5133
|
+
invalidateContentCache?: PluginContentCacheInvalidator,
|
|
5134
|
+
editorDispatch?: PluginEditorExtensionDispatch,
|
|
3667
5135
|
) {
|
|
3668
5136
|
if (!this.isPluginEnabled(pluginId)) {
|
|
3669
5137
|
return {
|
|
@@ -3671,11 +5139,40 @@ export class EmDashRuntime {
|
|
|
3671
5139
|
error: { code: "NOT_FOUND", message: `Plugin not enabled: ${pluginId}` },
|
|
3672
5140
|
};
|
|
3673
5141
|
}
|
|
5142
|
+
const normalizedMethod = method.toUpperCase();
|
|
5143
|
+
const routeMeta = this.getPluginRouteMeta(pluginId, path);
|
|
5144
|
+
if (routeMeta?.methods && !routeMeta.methods.some((allowed) => allowed === normalizedMethod)) {
|
|
5145
|
+
return {
|
|
5146
|
+
success: false,
|
|
5147
|
+
status: 405,
|
|
5148
|
+
error: { code: "METHOD_NOT_ALLOWED", message: "Method not allowed" },
|
|
5149
|
+
};
|
|
5150
|
+
}
|
|
3674
5151
|
|
|
3675
5152
|
// Authenticated caller for `ctx.user`. Undefined for public routes
|
|
3676
5153
|
// (the catch-all only forwards the caller after private-route auth)
|
|
3677
5154
|
// and for machine tokens with no bound user.
|
|
3678
5155
|
const caller = user ? toRouteCallerInfo(user) : undefined;
|
|
5156
|
+
let body: unknown;
|
|
5157
|
+
try {
|
|
5158
|
+
body = await parseRouteInput(request, routeMeta?.request);
|
|
5159
|
+
} catch (error) {
|
|
5160
|
+
if (error instanceof PluginRouteRequestError) {
|
|
5161
|
+
return {
|
|
5162
|
+
success: false,
|
|
5163
|
+
status: error.status,
|
|
5164
|
+
error: { code: "INVALID_PLUGIN_REQUEST", message: error.message },
|
|
5165
|
+
};
|
|
5166
|
+
}
|
|
5167
|
+
throw error;
|
|
5168
|
+
}
|
|
5169
|
+
const routeKey = path.replace(LEADING_SLASH_PATTERN, "");
|
|
5170
|
+
const adminDefinition =
|
|
5171
|
+
!editorDispatch && routeKey === "admin" ? this.getSandboxedAdminDefinition(pluginId) : null;
|
|
5172
|
+
const uiResult = adminDefinition
|
|
5173
|
+
? this.resolvePluginUiContext(adminDefinition, body, request)
|
|
5174
|
+
: {};
|
|
5175
|
+
if (uiResult.error) return { success: false, status: 400, error: uiResult.error };
|
|
3679
5176
|
|
|
3680
5177
|
// Check trusted (configured) plugins first — this must match the
|
|
3681
5178
|
// resolution order in getPluginRouteMeta to avoid auth/execution mismatches.
|
|
@@ -3683,24 +5180,44 @@ export class EmDashRuntime {
|
|
|
3683
5180
|
if (trustedPlugin && this.enabledPlugins.has(trustedPlugin.id)) {
|
|
3684
5181
|
const routeRegistry = new PluginRouteRegistry({
|
|
3685
5182
|
...this.pipelineFactoryOptions,
|
|
5183
|
+
contentActions: this.contentActionsWithInvalidator(invalidateContentCache),
|
|
3686
5184
|
emailPipeline: this.email ?? undefined,
|
|
3687
5185
|
cronReschedule: () => this.cronScheduler?.reschedule(),
|
|
3688
5186
|
trustedProxyHeaders: getTrustedProxyHeaders(this.config),
|
|
3689
5187
|
});
|
|
3690
5188
|
routeRegistry.register(trustedPlugin);
|
|
3691
5189
|
|
|
3692
|
-
const
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
5190
|
+
const result = await routeRegistry.invoke(pluginId, routeKey, {
|
|
5191
|
+
request,
|
|
5192
|
+
body,
|
|
5193
|
+
user: caller,
|
|
5194
|
+
ui: editorDispatch?.ui ?? uiResult.context,
|
|
5195
|
+
});
|
|
5196
|
+
return editorDispatch
|
|
5197
|
+
? this.validatePluginEditorExtensionResponse(pluginId, editorDispatch, result)
|
|
5198
|
+
: adminDefinition
|
|
5199
|
+
? this.validateSandboxedAdminResponse(pluginId, adminDefinition, result)
|
|
5200
|
+
: result;
|
|
3698
5201
|
}
|
|
3699
5202
|
|
|
3700
5203
|
// Check sandboxed (marketplace) plugins second
|
|
3701
5204
|
const sandboxedPlugin = this.findSandboxedPlugin(pluginId);
|
|
3702
5205
|
if (sandboxedPlugin) {
|
|
3703
|
-
|
|
5206
|
+
const result = await this.handleSandboxedRoute(
|
|
5207
|
+
sandboxedPlugin,
|
|
5208
|
+
path,
|
|
5209
|
+
request,
|
|
5210
|
+
body,
|
|
5211
|
+
routeMeta ?? { public: false },
|
|
5212
|
+
caller,
|
|
5213
|
+
editorDispatch?.ui ?? uiResult.context,
|
|
5214
|
+
invalidateContentCache,
|
|
5215
|
+
);
|
|
5216
|
+
return editorDispatch
|
|
5217
|
+
? this.validatePluginEditorExtensionResponse(pluginId, editorDispatch, result)
|
|
5218
|
+
: adminDefinition
|
|
5219
|
+
? this.validateSandboxedAdminResponse(pluginId, adminDefinition, result)
|
|
5220
|
+
: result;
|
|
3704
5221
|
}
|
|
3705
5222
|
|
|
3706
5223
|
return {
|
|
@@ -3709,6 +5226,28 @@ export class EmDashRuntime {
|
|
|
3709
5226
|
};
|
|
3710
5227
|
}
|
|
3711
5228
|
|
|
5229
|
+
private contentActionsWithInvalidator(
|
|
5230
|
+
invalidateContentCache?: PluginContentCacheInvalidator,
|
|
5231
|
+
): ContentActionCallbacks | undefined {
|
|
5232
|
+
const actions = this.pipelineFactoryOptions.contentActions;
|
|
5233
|
+
if (!actions || !invalidateContentCache) return actions;
|
|
5234
|
+
return {
|
|
5235
|
+
...actions,
|
|
5236
|
+
begin: (pluginId, invocationId, invocationInvalidator) =>
|
|
5237
|
+
actions.begin?.(pluginId, invocationId, invocationInvalidator ?? invalidateContentCache),
|
|
5238
|
+
publish: (pluginId, collection, id, options, invocationId) =>
|
|
5239
|
+
actions.publish(pluginId, collection, id, options, invocationId, invalidateContentCache),
|
|
5240
|
+
unpublish: (pluginId, collection, id, options, invocationId) =>
|
|
5241
|
+
actions.unpublish(pluginId, collection, id, options, invocationId, invalidateContentCache),
|
|
5242
|
+
schedule: (pluginId, collection, id, options, invocationId) =>
|
|
5243
|
+
actions.schedule(pluginId, collection, id, options, invocationId, invalidateContentCache),
|
|
5244
|
+
unschedule: (pluginId, collection, id, options, invocationId) =>
|
|
5245
|
+
actions.unschedule(pluginId, collection, id, options, invocationId, invalidateContentCache),
|
|
5246
|
+
restore: (pluginId, collection, id, options, invocationId) =>
|
|
5247
|
+
actions.restore(pluginId, collection, id, options, invocationId, invalidateContentCache),
|
|
5248
|
+
};
|
|
5249
|
+
}
|
|
5250
|
+
|
|
3712
5251
|
async getPluginMcpTools(pluginId?: string) {
|
|
3713
5252
|
const tools: Array<{
|
|
3714
5253
|
pluginId: string;
|
|
@@ -3726,7 +5265,14 @@ export class EmDashRuntime {
|
|
|
3726
5265
|
if (pluginId && plugin.id !== pluginId) continue;
|
|
3727
5266
|
for (const [name, tool] of Object.entries(plugin.mcp?.tools ?? {})) {
|
|
3728
5267
|
const route = plugin.routes[tool.route];
|
|
3729
|
-
if (
|
|
5268
|
+
if (
|
|
5269
|
+
!route ||
|
|
5270
|
+
route.public ||
|
|
5271
|
+
route.response === "raw" ||
|
|
5272
|
+
!isJsonPostRouteContract(route) ||
|
|
5273
|
+
!route.permission ||
|
|
5274
|
+
!Object.hasOwn(Permissions, route.permission)
|
|
5275
|
+
)
|
|
3730
5276
|
continue;
|
|
3731
5277
|
const key = `${plugin.id}__${name}`;
|
|
3732
5278
|
if (seen.has(key)) continue;
|
|
@@ -3753,8 +5299,10 @@ export class EmDashRuntime {
|
|
|
3753
5299
|
seen.has(key) ||
|
|
3754
5300
|
!routeMeta ||
|
|
3755
5301
|
routeMeta.public ||
|
|
5302
|
+
routeMeta.response === "raw" ||
|
|
5303
|
+
!isJsonPostRouteContract(routeMeta) ||
|
|
3756
5304
|
routeMeta.permission !== tool.permission ||
|
|
3757
|
-
!(tool.permission
|
|
5305
|
+
!Object.hasOwn(Permissions, tool.permission)
|
|
3758
5306
|
) {
|
|
3759
5307
|
continue;
|
|
3760
5308
|
}
|
|
@@ -3828,6 +5376,7 @@ export class EmDashRuntime {
|
|
|
3828
5376
|
actorId: string,
|
|
3829
5377
|
request: Request,
|
|
3830
5378
|
caller?: RouteCallerInput | null,
|
|
5379
|
+
invalidateContentCache?: PluginContentCacheInvalidator,
|
|
3831
5380
|
) {
|
|
3832
5381
|
const requestMeta = extractRequestMeta(request, getTrustedProxyHeaders(this.config));
|
|
3833
5382
|
const audit = new AuditRepository(this.db);
|
|
@@ -3845,6 +5394,7 @@ export class EmDashRuntime {
|
|
|
3845
5394
|
route,
|
|
3846
5395
|
internalRequest,
|
|
3847
5396
|
caller,
|
|
5397
|
+
invalidateContentCache,
|
|
3848
5398
|
);
|
|
3849
5399
|
await audit.log({
|
|
3850
5400
|
actorId,
|
|
@@ -3891,6 +5441,37 @@ export class EmDashRuntime {
|
|
|
3891
5441
|
return undefined;
|
|
3892
5442
|
}
|
|
3893
5443
|
|
|
5444
|
+
/**
|
|
5445
|
+
* Drop incoming keys that have no matching collection field when the entry
|
|
5446
|
+
* already stores them in its live data or current draft revision.
|
|
5447
|
+
*
|
|
5448
|
+
* Draft revisions keep the full `data` JSON, so deleting a field can strand
|
|
5449
|
+
* the old value; this lets a read-then-write save succeed without allowing
|
|
5450
|
+
* genuinely unknown keys.
|
|
5451
|
+
*/
|
|
5452
|
+
private async dropUnknownKeysAlreadyStored(
|
|
5453
|
+
data: Record<string, unknown>,
|
|
5454
|
+
existing: { data: Record<string, unknown>; draftRevisionId?: string | null } | null,
|
|
5455
|
+
knownFieldSlugs: ReadonlySet<string>,
|
|
5456
|
+
): Promise<Record<string, unknown>> {
|
|
5457
|
+
if (!existing) return data;
|
|
5458
|
+
|
|
5459
|
+
let stored: Record<string, unknown> = existing.data ?? {};
|
|
5460
|
+
if (existing.draftRevisionId) {
|
|
5461
|
+
const draft = await new RevisionRepository(this.db)
|
|
5462
|
+
.findById(existing.draftRevisionId)
|
|
5463
|
+
.catch(() => null);
|
|
5464
|
+
if (draft?.data) stored = draft.data;
|
|
5465
|
+
}
|
|
5466
|
+
|
|
5467
|
+
const stale = staleStoredKeys(data, stored, knownFieldSlugs);
|
|
5468
|
+
if (stale.length === 0) return data;
|
|
5469
|
+
|
|
5470
|
+
const result = { ...data };
|
|
5471
|
+
for (const key of stale) delete result[key];
|
|
5472
|
+
return result;
|
|
5473
|
+
}
|
|
5474
|
+
|
|
3894
5475
|
/**
|
|
3895
5476
|
* Normalize image/file fields in content data.
|
|
3896
5477
|
* Fills missing dimensions, storageKey, mimeType, and filename from providers.
|
|
@@ -3898,12 +5479,15 @@ export class EmDashRuntime {
|
|
|
3898
5479
|
private async normalizeMediaFields(
|
|
3899
5480
|
collection: string,
|
|
3900
5481
|
data: Record<string, unknown>,
|
|
5482
|
+
preloaded?: CollectionWithFields | null,
|
|
3901
5483
|
): Promise<Record<string, unknown>> {
|
|
3902
|
-
let collectionInfo;
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
5484
|
+
let collectionInfo = preloaded;
|
|
5485
|
+
if (collectionInfo === undefined) {
|
|
5486
|
+
try {
|
|
5487
|
+
collectionInfo = await this.schemaRegistry.getCollectionWithFields(collection);
|
|
5488
|
+
} catch {
|
|
5489
|
+
return data;
|
|
5490
|
+
}
|
|
3907
5491
|
}
|
|
3908
5492
|
if (!collectionInfo?.fields) return data;
|
|
3909
5493
|
|
|
@@ -3973,119 +5557,22 @@ export class EmDashRuntime {
|
|
|
3973
5557
|
return result;
|
|
3974
5558
|
}
|
|
3975
5559
|
|
|
3976
|
-
private async runSandboxedBeforeSave(
|
|
3977
|
-
content: Record<string, unknown>,
|
|
3978
|
-
collection: string,
|
|
3979
|
-
isNew: boolean,
|
|
3980
|
-
contentId?: string,
|
|
3981
|
-
) {
|
|
3982
|
-
let result = content;
|
|
3983
|
-
|
|
3984
|
-
for (const [pluginKey, plugin] of this.sandboxedPlugins) {
|
|
3985
|
-
const [id] = pluginKey.split(":");
|
|
3986
|
-
if (!id || !this.isPluginEnabled(id)) continue;
|
|
3987
|
-
|
|
3988
|
-
try {
|
|
3989
|
-
const event: ContentHookEvent = { content: result, collection, isNew };
|
|
3990
|
-
if (contentId !== undefined) event.id = contentId;
|
|
3991
|
-
const hookResult = await plugin.invokeHook("content:beforeSave", event);
|
|
3992
|
-
const inspection = inspectSandboxHookResult(hookResult);
|
|
3993
|
-
if (inspection.kind === "error") {
|
|
3994
|
-
return {
|
|
3995
|
-
success: false as const,
|
|
3996
|
-
error: {
|
|
3997
|
-
code: ErrorCode.SAVE_REJECTED,
|
|
3998
|
-
message: "Save rejected by a sandboxed plugin",
|
|
3999
|
-
details: { pluginId: id, reason: inspection.error.reason },
|
|
4000
|
-
},
|
|
4001
|
-
};
|
|
4002
|
-
}
|
|
4003
|
-
if (inspection.kind === "malformed") {
|
|
4004
|
-
console.error(`EmDash: Sandboxed plugin ${id} returned an invalid hook result`);
|
|
4005
|
-
return {
|
|
4006
|
-
success: false as const,
|
|
4007
|
-
error: {
|
|
4008
|
-
code: ErrorCode.CONTENT_HOOK_ERROR,
|
|
4009
|
-
message: "A plugin hook failed while saving content",
|
|
4010
|
-
},
|
|
4011
|
-
};
|
|
4012
|
-
}
|
|
4013
|
-
if (hookResult && typeof hookResult === "object" && !Array.isArray(hookResult)) {
|
|
4014
|
-
// Sandbox returns unknown; convert to record by iterating own properties
|
|
4015
|
-
const record: Record<string, unknown> = {};
|
|
4016
|
-
for (const [k, v] of Object.entries(hookResult)) {
|
|
4017
|
-
record[k] = v;
|
|
4018
|
-
}
|
|
4019
|
-
result = record;
|
|
4020
|
-
}
|
|
4021
|
-
} catch (error) {
|
|
4022
|
-
console.error(`EmDash: Sandboxed plugin ${id} beforeSave hook failed:`, error);
|
|
4023
|
-
return {
|
|
4024
|
-
success: false as const,
|
|
4025
|
-
error: {
|
|
4026
|
-
code: ErrorCode.CONTENT_HOOK_ERROR,
|
|
4027
|
-
message: "A plugin hook failed while saving content",
|
|
4028
|
-
},
|
|
4029
|
-
};
|
|
4030
|
-
}
|
|
4031
|
-
}
|
|
4032
|
-
|
|
4033
|
-
return { success: true as const, data: result };
|
|
4034
|
-
}
|
|
4035
|
-
|
|
4036
|
-
private async runSandboxedBeforeDelete(id: string, collection: string): Promise<boolean> {
|
|
4037
|
-
for (const [pluginKey, plugin] of this.sandboxedPlugins) {
|
|
4038
|
-
const [pluginId] = pluginKey.split(":");
|
|
4039
|
-
if (!pluginId || !this.isPluginEnabled(pluginId)) continue;
|
|
4040
|
-
|
|
4041
|
-
try {
|
|
4042
|
-
const result = await plugin.invokeHook("content:beforeDelete", {
|
|
4043
|
-
id,
|
|
4044
|
-
collection,
|
|
4045
|
-
});
|
|
4046
|
-
if (result === false) {
|
|
4047
|
-
return false;
|
|
4048
|
-
}
|
|
4049
|
-
} catch (error) {
|
|
4050
|
-
console.error(`EmDash: Sandboxed plugin ${pluginId} beforeDelete hook error:`, error);
|
|
4051
|
-
}
|
|
4052
|
-
}
|
|
4053
|
-
|
|
4054
|
-
return true;
|
|
4055
|
-
}
|
|
4056
|
-
|
|
4057
5560
|
private runAfterSaveHooks(
|
|
4058
5561
|
content: Record<string, unknown>,
|
|
4059
5562
|
collection: string,
|
|
4060
5563
|
isNew: boolean,
|
|
5564
|
+
actor?: ActorInfo,
|
|
5565
|
+
excludePluginId?: string,
|
|
4061
5566
|
): void {
|
|
4062
5567
|
after(async () => {
|
|
4063
5568
|
// Trusted plugins
|
|
4064
5569
|
if (this.hooks.hasHooks("content:afterSave")) {
|
|
4065
5570
|
try {
|
|
4066
|
-
await this.hooks.runContentAfterSave(content, collection, isNew);
|
|
5571
|
+
await this.hooks.runContentAfterSave(content, collection, isNew, actor, excludePluginId);
|
|
4067
5572
|
} catch (err) {
|
|
4068
5573
|
console.error("EmDash afterSave hook error:", err);
|
|
4069
5574
|
}
|
|
4070
5575
|
}
|
|
4071
|
-
|
|
4072
|
-
// Sandboxed plugins
|
|
4073
|
-
const tasks: Promise<void>[] = [];
|
|
4074
|
-
for (const [pluginKey, plugin] of this.sandboxedPlugins) {
|
|
4075
|
-
const [id] = pluginKey.split(":");
|
|
4076
|
-
if (!id || !this.isPluginEnabled(id)) continue;
|
|
4077
|
-
|
|
4078
|
-
tasks.push(
|
|
4079
|
-
(async () => {
|
|
4080
|
-
try {
|
|
4081
|
-
await plugin.invokeHook("content:afterSave", { content, collection, isNew });
|
|
4082
|
-
} catch (err) {
|
|
4083
|
-
console.error(`EmDash: Sandboxed plugin ${id} afterSave error:`, err);
|
|
4084
|
-
}
|
|
4085
|
-
})(),
|
|
4086
|
-
);
|
|
4087
|
-
}
|
|
4088
|
-
await Promise.allSettled(tasks);
|
|
4089
5576
|
});
|
|
4090
5577
|
}
|
|
4091
5578
|
|
|
@@ -4099,24 +5586,6 @@ export class EmDashRuntime {
|
|
|
4099
5586
|
console.error("EmDash afterDelete hook error:", err);
|
|
4100
5587
|
}
|
|
4101
5588
|
}
|
|
4102
|
-
|
|
4103
|
-
// Sandboxed plugins
|
|
4104
|
-
const tasks: Promise<void>[] = [];
|
|
4105
|
-
for (const [pluginKey, plugin] of this.sandboxedPlugins) {
|
|
4106
|
-
const [pluginId] = pluginKey.split(":");
|
|
4107
|
-
if (!pluginId || !this.isPluginEnabled(pluginId)) continue;
|
|
4108
|
-
|
|
4109
|
-
tasks.push(
|
|
4110
|
-
(async () => {
|
|
4111
|
-
try {
|
|
4112
|
-
await plugin.invokeHook("content:afterDelete", { id, collection, permanent });
|
|
4113
|
-
} catch (err) {
|
|
4114
|
-
console.error(`EmDash: Sandboxed plugin ${pluginId} afterDelete error:`, err);
|
|
4115
|
-
}
|
|
4116
|
-
})(),
|
|
4117
|
-
);
|
|
4118
|
-
}
|
|
4119
|
-
await Promise.allSettled(tasks);
|
|
4120
5589
|
});
|
|
4121
5590
|
}
|
|
4122
5591
|
|
|
@@ -4129,10 +5598,12 @@ export class EmDashRuntime {
|
|
|
4129
5598
|
| "content:afterUnschedule",
|
|
4130
5599
|
content: Record<string, unknown>,
|
|
4131
5600
|
collection: string,
|
|
5601
|
+
afterPluginId?: string,
|
|
5602
|
+
pluginInvocationId?: string,
|
|
4132
5603
|
): void {
|
|
4133
5604
|
const label = name.slice("content:".length);
|
|
4134
5605
|
|
|
4135
|
-
|
|
5606
|
+
const invoke = async () => {
|
|
4136
5607
|
// Trusted plugins
|
|
4137
5608
|
if (this.hooks.hasHooks(name)) {
|
|
4138
5609
|
try {
|
|
@@ -4157,52 +5628,160 @@ export class EmDashRuntime {
|
|
|
4157
5628
|
console.error(`EmDash ${label} hook error:`, err);
|
|
4158
5629
|
}
|
|
4159
5630
|
}
|
|
5631
|
+
};
|
|
5632
|
+
const contentId = typeof content.id === "string" ? content.id : undefined;
|
|
5633
|
+
const guardKey =
|
|
5634
|
+
afterPluginId && contentId ? this.pluginContentActionKey(collection, contentId) : undefined;
|
|
5635
|
+
const invokeWithGuard = async () => {
|
|
5636
|
+
if (guardKey) this.retainPluginContentAction(guardKey);
|
|
5637
|
+
try {
|
|
5638
|
+
await invoke();
|
|
5639
|
+
} finally {
|
|
5640
|
+
if (guardKey) this.releasePluginContentAction(guardKey);
|
|
5641
|
+
}
|
|
5642
|
+
};
|
|
5643
|
+
if (afterPluginId && pluginInvocationId && this.findSandboxedPlugin(afterPluginId)) {
|
|
5644
|
+
const key = this.pluginInvocationKey(afterPluginId, pluginInvocationId);
|
|
5645
|
+
if (!this.activePluginInvocations.has(key) || this.releasedPluginInvocations.has(key)) {
|
|
5646
|
+
after(invokeWithGuard);
|
|
5647
|
+
return;
|
|
5648
|
+
}
|
|
5649
|
+
const pending = this.pendingPluginAfterHooks.get(key) ?? [];
|
|
5650
|
+
pending.push(invokeWithGuard);
|
|
5651
|
+
this.pendingPluginAfterHooks.set(key, pending);
|
|
5652
|
+
return;
|
|
5653
|
+
}
|
|
5654
|
+
after(invokeWithGuard);
|
|
5655
|
+
}
|
|
4160
5656
|
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
const [pluginId] = pluginKey.split(":");
|
|
4165
|
-
if (!pluginId || !this.isPluginEnabled(pluginId)) continue;
|
|
5657
|
+
private pluginInvocationKey(pluginId: string, invocationId: string): string {
|
|
5658
|
+
return JSON.stringify([pluginId, invocationId]);
|
|
5659
|
+
}
|
|
4166
5660
|
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
5661
|
+
private beginPluginInvocation(
|
|
5662
|
+
pluginId: string,
|
|
5663
|
+
invocationId: string,
|
|
5664
|
+
invalidateContentCache?: PluginContentCacheInvalidator,
|
|
5665
|
+
): void {
|
|
5666
|
+
const key = this.pluginInvocationKey(pluginId, invocationId);
|
|
5667
|
+
this.pendingPluginAfterHooks.delete(key);
|
|
5668
|
+
this.releasedPluginInvocations.delete(key);
|
|
5669
|
+
if (invalidateContentCache)
|
|
5670
|
+
this.pluginInvocationCacheInvalidators.set(key, invalidateContentCache);
|
|
5671
|
+
else this.pluginInvocationCacheInvalidators.delete(key);
|
|
5672
|
+
this.activePluginInvocations.add(key);
|
|
5673
|
+
}
|
|
5674
|
+
|
|
5675
|
+
private flushPluginAfterHooks(
|
|
5676
|
+
pluginId: string,
|
|
5677
|
+
invocationId?: string,
|
|
5678
|
+
final = true,
|
|
5679
|
+
): Promise<void> {
|
|
5680
|
+
if (!invocationId) return Promise.resolve();
|
|
5681
|
+
const key = this.pluginInvocationKey(pluginId, invocationId);
|
|
5682
|
+
this.releasedPluginInvocations.add(key);
|
|
5683
|
+
const pending = this.pendingPluginAfterHooks.get(key) ?? [];
|
|
5684
|
+
this.pendingPluginAfterHooks.delete(key);
|
|
5685
|
+
for (const invoke of pending) after(invoke);
|
|
5686
|
+
if (final) {
|
|
5687
|
+
this.releasedPluginInvocations.delete(key);
|
|
5688
|
+
this.activePluginInvocations.delete(key);
|
|
5689
|
+
this.pluginInvocationCacheInvalidators.delete(key);
|
|
5690
|
+
} else {
|
|
5691
|
+
const timer = setTimeout(() => {
|
|
5692
|
+
this.releasedPluginInvocations.delete(key);
|
|
5693
|
+
this.activePluginInvocations.delete(key);
|
|
5694
|
+
this.pluginInvocationCacheInvalidators.delete(key);
|
|
5695
|
+
}, PLUGIN_INVOCATION_RELEASE_GRACE_MS);
|
|
5696
|
+
if (typeof timer === "object") timer.unref();
|
|
5697
|
+
}
|
|
5698
|
+
return Promise.resolve();
|
|
4179
5699
|
}
|
|
4180
5700
|
|
|
4181
|
-
private runAfterPublishHooks(
|
|
4182
|
-
|
|
5701
|
+
private runAfterPublishHooks(
|
|
5702
|
+
content: Record<string, unknown>,
|
|
5703
|
+
collection: string,
|
|
5704
|
+
afterPluginId?: string,
|
|
5705
|
+
pluginInvocationId?: string,
|
|
5706
|
+
): void {
|
|
5707
|
+
this.runDeferredContentHook(
|
|
5708
|
+
"content:afterPublish",
|
|
5709
|
+
content,
|
|
5710
|
+
collection,
|
|
5711
|
+
afterPluginId,
|
|
5712
|
+
pluginInvocationId,
|
|
5713
|
+
);
|
|
4183
5714
|
}
|
|
4184
5715
|
|
|
4185
|
-
private runAfterUnpublishHooks(
|
|
4186
|
-
|
|
5716
|
+
private runAfterUnpublishHooks(
|
|
5717
|
+
content: Record<string, unknown>,
|
|
5718
|
+
collection: string,
|
|
5719
|
+
afterPluginId?: string,
|
|
5720
|
+
pluginInvocationId?: string,
|
|
5721
|
+
): void {
|
|
5722
|
+
this.runDeferredContentHook(
|
|
5723
|
+
"content:afterUnpublish",
|
|
5724
|
+
content,
|
|
5725
|
+
collection,
|
|
5726
|
+
afterPluginId,
|
|
5727
|
+
pluginInvocationId,
|
|
5728
|
+
);
|
|
4187
5729
|
}
|
|
4188
5730
|
|
|
4189
|
-
private runAfterRestoreHooks(
|
|
4190
|
-
|
|
5731
|
+
private runAfterRestoreHooks(
|
|
5732
|
+
content: Record<string, unknown>,
|
|
5733
|
+
collection: string,
|
|
5734
|
+
afterPluginId?: string,
|
|
5735
|
+
pluginInvocationId?: string,
|
|
5736
|
+
): void {
|
|
5737
|
+
this.runDeferredContentHook(
|
|
5738
|
+
"content:afterRestore",
|
|
5739
|
+
content,
|
|
5740
|
+
collection,
|
|
5741
|
+
afterPluginId,
|
|
5742
|
+
pluginInvocationId,
|
|
5743
|
+
);
|
|
4191
5744
|
}
|
|
4192
5745
|
|
|
4193
|
-
private runAfterScheduleHooks(
|
|
4194
|
-
|
|
5746
|
+
private runAfterScheduleHooks(
|
|
5747
|
+
content: Record<string, unknown>,
|
|
5748
|
+
collection: string,
|
|
5749
|
+
afterPluginId?: string,
|
|
5750
|
+
pluginInvocationId?: string,
|
|
5751
|
+
): void {
|
|
5752
|
+
this.runDeferredContentHook(
|
|
5753
|
+
"content:afterSchedule",
|
|
5754
|
+
content,
|
|
5755
|
+
collection,
|
|
5756
|
+
afterPluginId,
|
|
5757
|
+
pluginInvocationId,
|
|
5758
|
+
);
|
|
4195
5759
|
}
|
|
4196
5760
|
|
|
4197
|
-
private runAfterUnscheduleHooks(
|
|
4198
|
-
|
|
5761
|
+
private runAfterUnscheduleHooks(
|
|
5762
|
+
content: Record<string, unknown>,
|
|
5763
|
+
collection: string,
|
|
5764
|
+
afterPluginId?: string,
|
|
5765
|
+
pluginInvocationId?: string,
|
|
5766
|
+
): void {
|
|
5767
|
+
this.runDeferredContentHook(
|
|
5768
|
+
"content:afterUnschedule",
|
|
5769
|
+
content,
|
|
5770
|
+
collection,
|
|
5771
|
+
afterPluginId,
|
|
5772
|
+
pluginInvocationId,
|
|
5773
|
+
);
|
|
4199
5774
|
}
|
|
4200
5775
|
|
|
4201
5776
|
private async handleSandboxedRoute(
|
|
4202
5777
|
plugin: SandboxedPluginInstance,
|
|
4203
5778
|
path: string,
|
|
4204
5779
|
request: Request,
|
|
5780
|
+
body: unknown,
|
|
5781
|
+
routeMeta: RouteMeta,
|
|
4205
5782
|
user?: UserInfo,
|
|
5783
|
+
ui?: PluginUiContext,
|
|
5784
|
+
invalidateContentCache?: PluginContentCacheInvalidator,
|
|
4206
5785
|
): Promise<{
|
|
4207
5786
|
success: boolean;
|
|
4208
5787
|
data?: unknown;
|
|
@@ -4211,21 +5790,32 @@ export class EmDashRuntime {
|
|
|
4211
5790
|
}> {
|
|
4212
5791
|
const routeName = path.replace(LEADING_SLASH_PATTERN, "");
|
|
4213
5792
|
|
|
4214
|
-
// Body methods parse JSON; GET/HEAD/DELETE parse the query string (#2146).
|
|
4215
|
-
const body = await parseRouteInput(request);
|
|
4216
|
-
|
|
4217
5793
|
try {
|
|
4218
|
-
const
|
|
5794
|
+
const declaredHeaders = routeMeta.request ? (routeMeta.request.headers ?? []) : undefined;
|
|
5795
|
+
const headers = sanitizeHeadersForSandbox(request.headers, declaredHeaders);
|
|
4219
5796
|
const meta = extractRequestMeta(request, this.config);
|
|
4220
|
-
const result = await plugin.invokeRoute(
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
5797
|
+
const result = await plugin.invokeRoute(
|
|
5798
|
+
routeName,
|
|
5799
|
+
body,
|
|
5800
|
+
{
|
|
5801
|
+
url: request.url,
|
|
5802
|
+
method: request.method,
|
|
5803
|
+
headers,
|
|
5804
|
+
meta,
|
|
5805
|
+
user,
|
|
5806
|
+
ui,
|
|
5807
|
+
},
|
|
5808
|
+
{ invalidateContentCache },
|
|
5809
|
+
);
|
|
4227
5810
|
return { success: true, data: result };
|
|
4228
5811
|
} catch (error) {
|
|
5812
|
+
if (error instanceof PluginRouteRequestError) {
|
|
5813
|
+
return {
|
|
5814
|
+
success: false,
|
|
5815
|
+
status: error.status,
|
|
5816
|
+
error: { code: "INVALID_PLUGIN_REQUEST", message: error.message },
|
|
5817
|
+
};
|
|
5818
|
+
}
|
|
4229
5819
|
console.error(`EmDash: Sandboxed plugin route error:`, error);
|
|
4230
5820
|
const sandboxRouteError = getSandboxRouteErrorDetails(error);
|
|
4231
5821
|
if (sandboxRouteError) {
|
|
@@ -4292,26 +5882,6 @@ export class EmDashRuntime {
|
|
|
4292
5882
|
}
|
|
4293
5883
|
}
|
|
4294
5884
|
|
|
4295
|
-
// Sandboxed plugins — metadata only, never fragments
|
|
4296
|
-
for (const [pluginKey, plugin] of this.sandboxedPlugins) {
|
|
4297
|
-
const [id] = pluginKey.split(":");
|
|
4298
|
-
if (!id || !this.isPluginEnabled(id)) continue;
|
|
4299
|
-
|
|
4300
|
-
try {
|
|
4301
|
-
const result = await plugin.invokeHook("page:metadata", { page });
|
|
4302
|
-
if (result != null) {
|
|
4303
|
-
const items = Array.isArray(result) ? result : [result];
|
|
4304
|
-
for (const item of items) {
|
|
4305
|
-
if (isValidMetadataContribution(item)) {
|
|
4306
|
-
metadata.push(item);
|
|
4307
|
-
}
|
|
4308
|
-
}
|
|
4309
|
-
}
|
|
4310
|
-
} catch (error) {
|
|
4311
|
-
console.error(`EmDash: Sandboxed plugin ${id} page:metadata error:`, error);
|
|
4312
|
-
}
|
|
4313
|
-
}
|
|
4314
|
-
|
|
4315
5885
|
return { metadata, fragments };
|
|
4316
5886
|
}
|
|
4317
5887
|
|