emdash 0.40.0 → 0.41.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-CbKDcPJi.mjs +26 -0
- package/dist/action-token-CbKDcPJi.mjs.map +1 -0
- package/dist/activation-PtniBLt9.mjs +382 -0
- package/dist/activation-PtniBLt9.mjs.map +1 -0
- package/dist/adapters-C_Tf5x90.d.mts +171 -0
- package/dist/adapters-C_Tf5x90.d.mts.map +1 -0
- package/dist/allowed-origins-U1z4TdEZ.mjs +116 -0
- package/dist/allowed-origins-U1z4TdEZ.mjs.map +1 -0
- package/dist/api/route-utils.d.mts +4 -4
- package/dist/api/route-utils.mjs +46 -45
- package/dist/api/route-utils.mjs.map +1 -1
- package/dist/api/schemas/index.d.mts +2 -2
- package/dist/api/schemas/index.mjs +10 -10
- package/dist/api-CXwQkV_m.mjs +5122 -0
- package/dist/api-CXwQkV_m.mjs.map +1 -0
- package/dist/api-tokens-89WlohEN.mjs +166 -0
- package/dist/api-tokens-89WlohEN.mjs.map +1 -0
- package/dist/apply-Do3KNnUk.mjs +1659 -0
- package/dist/apply-Do3KNnUk.mjs.map +1 -0
- package/dist/approvals-CAX9Yw33.mjs +460 -0
- package/dist/approvals-CAX9Yw33.mjs.map +1 -0
- package/dist/artifact-fetch-B9W4CEST.mjs +659 -0
- package/dist/artifact-fetch-B9W4CEST.mjs.map +1 -0
- package/dist/astro/index.d.mts +14 -14
- package/dist/astro/index.mjs +26 -30
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +12 -12
- package/dist/astro/middleware/auth.mjs +10 -10
- package/dist/astro/middleware/media-usage-write-fence.mjs +5 -5
- package/dist/astro/middleware/redirect.mjs +7 -7
- package/dist/astro/middleware/request-context.mjs +3 -3
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.d.mts +12 -12
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +110 -106
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +14 -14
- package/dist/astro/routes/api/admin/allowed-domains/index.mjs +14 -14
- package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +6 -6
- package/dist/astro/routes/api/admin/api-tokens/index.mjs +7 -7
- package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +7 -7
- package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +17 -17
- package/dist/astro/routes/api/admin/byline-fields/index.mjs +17 -17
- package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +17 -17
- package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +23 -23
- package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +23 -23
- package/dist/astro/routes/api/admin/bylines/index.mjs +23 -23
- package/dist/astro/routes/api/admin/comments/_id_/status.mjs +18 -18
- package/dist/astro/routes/api/admin/comments/_id_.mjs +7 -7
- package/dist/astro/routes/api/admin/comments/bulk.mjs +17 -17
- package/dist/astro/routes/api/admin/comments/counts.mjs +7 -7
- package/dist/astro/routes/api/admin/comments/index.mjs +17 -17
- 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 +20 -20
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.mjs +22 -22
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.mjs +22 -22
- package/dist/astro/routes/api/admin/media-usage/progress.mjs +25 -25
- package/dist/astro/routes/api/admin/media-usage/repair.mjs +31 -31
- package/dist/astro/routes/api/admin/media-usage/work/index.mjs +22 -22
- package/dist/astro/routes/api/admin/media-usage/work/retry.mjs +22 -22
- package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +6 -6
- package/dist/astro/routes/api/admin/oauth-clients/index.mjs +6 -6
- package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +8 -8
- package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +8 -8
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +72 -68
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/mcp.mjs +7 -7
- package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs +71 -67
- package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +72 -68
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +82 -78
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/index.mjs +72 -68
- package/dist/astro/routes/api/admin/plugins/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +5 -5
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +71 -67
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +81 -77
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +71 -67
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +72 -68
- package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +83 -79
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +9 -9
- package/dist/astro/routes/api/admin/plugins/registry/install.mjs +74 -70
- package/dist/astro/routes/api/admin/plugins/registry/install.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/publisher-handle.mjs +7 -7
- package/dist/astro/routes/api/admin/plugins/registry/verify.mjs +72 -68
- package/dist/astro/routes/api/admin/plugins/registry/verify.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/updates.mjs +71 -67
- package/dist/astro/routes/api/admin/plugins/updates.mjs.map +1 -1
- package/dist/astro/routes/api/admin/scheduled-policy-rejections/_collection_/_id_.mjs +6 -6
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +71 -67
- 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 +5 -5
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +71 -67
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/transfer/_...path_.mjs +4 -4
- package/dist/astro/routes/api/admin/transfer/approvals/_id_/approve.mjs +40 -40
- package/dist/astro/routes/api/admin/transfer/approvals/_id_/deny.mjs +40 -40
- package/dist/astro/routes/api/admin/transfer/approvals/index.mjs +42 -42
- package/dist/astro/routes/api/admin/transfer/capabilities.mjs +42 -42
- package/dist/astro/routes/api/admin/transfer/exports/_id_/advance.mjs +42 -42
- package/dist/astro/routes/api/admin/transfer/exports/_id_/archive.mjs +43 -43
- package/dist/astro/routes/api/admin/transfer/exports/_id_/files/_...path_.mjs +43 -43
- package/dist/astro/routes/api/admin/transfer/exports/_id_/index.mjs +42 -42
- package/dist/astro/routes/api/admin/transfer/exports/_id_/manifest.mjs +43 -43
- package/dist/astro/routes/api/admin/transfer/exports/index.mjs +44 -44
- package/dist/astro/routes/api/admin/transfer/imports/_id_/abandon.mjs +42 -42
- package/dist/astro/routes/api/admin/transfer/imports/_id_/advance.mjs +42 -42
- package/dist/astro/routes/api/admin/transfer/imports/_id_/analyze.mjs +44 -44
- package/dist/astro/routes/api/admin/transfer/imports/_id_/cancel.mjs +42 -42
- package/dist/astro/routes/api/admin/transfer/imports/_id_/execute.mjs +44 -44
- package/dist/astro/routes/api/admin/transfer/imports/_id_/files/_...path_.mjs +42 -42
- package/dist/astro/routes/api/admin/transfer/imports/_id_/index.mjs +42 -42
- package/dist/astro/routes/api/admin/transfer/imports/_id_/missing.mjs +44 -44
- package/dist/astro/routes/api/admin/transfer/imports/_id_/plan.mjs +42 -42
- package/dist/astro/routes/api/admin/transfer/imports/_id_/receipt.mjs +42 -42
- package/dist/astro/routes/api/admin/transfer/imports/index.mjs +44 -44
- package/dist/astro/routes/api/admin/users/_id_/disable.mjs +5 -5
- package/dist/astro/routes/api/admin/users/_id_/enable.mjs +4 -4
- package/dist/astro/routes/api/admin/users/_id_/index.mjs +15 -15
- package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +6 -6
- package/dist/astro/routes/api/admin/users/index.mjs +14 -14
- package/dist/astro/routes/api/auth/dev-bypass.mjs +6 -6
- package/dist/astro/routes/api/auth/invite/accept.mjs +4 -4
- package/dist/astro/routes/api/auth/invite/complete.mjs +18 -18
- package/dist/astro/routes/api/auth/invite/index.mjs +16 -16
- package/dist/astro/routes/api/auth/invite/register-options.mjs +17 -17
- package/dist/astro/routes/api/auth/logout.mjs +5 -5
- package/dist/astro/routes/api/auth/magic-link/send.mjs +18 -18
- package/dist/astro/routes/api/auth/magic-link/verify.mjs +14 -14
- package/dist/astro/routes/api/auth/me.mjs +15 -15
- package/dist/astro/routes/api/auth/mode.mjs +5 -5
- package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs +3 -3
- package/dist/astro/routes/api/auth/oauth/_provider_.mjs +2 -2
- package/dist/astro/routes/api/auth/passkey/_id_.mjs +14 -14
- package/dist/astro/routes/api/auth/passkey/index.mjs +4 -4
- package/dist/astro/routes/api/auth/passkey/options.mjs +19 -19
- package/dist/astro/routes/api/auth/passkey/register/options.mjs +17 -17
- package/dist/astro/routes/api/auth/passkey/register/verify.mjs +18 -18
- package/dist/astro/routes/api/auth/passkey/verify.mjs +18 -18
- package/dist/astro/routes/api/auth/signup/complete.mjs +18 -18
- package/dist/astro/routes/api/auth/signup/request.mjs +17 -17
- package/dist/astro/routes/api/auth/signup/verify.mjs +4 -4
- package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +12 -12
- package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs +18 -18
- package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +5 -5
- package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +17 -17
- package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +5 -5
- package/dist/astro/routes/api/content/_collection_/_id_/lock.mjs +17 -17
- package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +5 -5
- package/dist/astro/routes/api/content/_collection_/_id_/plugin-extensions/_pluginId_/_kind_/_extensionId_.mjs +11 -11
- package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +31 -31
- package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +17 -17
- package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.d.mts +1 -2
- package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.d.mts.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs +42 -53
- 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 +34 -29
- 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 +5 -5
- package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +17 -17
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +25 -24
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +5 -5
- package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +17 -17
- package/dist/astro/routes/api/content/_collection_/_id_.d.mts.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_.mjs +18 -18
- package/dist/astro/routes/api/content/_collection_/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/authors.mjs +5 -5
- package/dist/astro/routes/api/content/_collection_/index.mjs +15 -15
- package/dist/astro/routes/api/content/_collection_/trash.mjs +15 -15
- package/dist/astro/routes/api/dashboard.mjs +10 -9
- package/dist/astro/routes/api/dashboard.mjs.map +1 -1
- package/dist/astro/routes/api/dev/emails.mjs +5 -5
- package/dist/astro/routes/api/health.mjs +6 -6
- package/dist/astro/routes/api/import/probe.d.mts +3 -3
- package/dist/astro/routes/api/import/probe.mjs +23 -23
- package/dist/astro/routes/api/import/wordpress/analyze.mjs +6 -6
- package/dist/astro/routes/api/import/wordpress/execute.d.mts +13 -13
- package/dist/astro/routes/api/import/wordpress/execute.mjs +16 -16
- package/dist/astro/routes/api/import/wordpress/media.d.mts +12 -12
- package/dist/astro/routes/api/import/wordpress/media.mjs +19 -19
- package/dist/astro/routes/api/import/wordpress/prepare.mjs +18 -18
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts +13 -13
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +28 -28
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.d.mts +1 -1
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +23 -23
- package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +13 -13
- package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +34 -34
- package/dist/astro/routes/api/manifest.mjs +6 -6
- package/dist/astro/routes/api/mcp.mjs +98 -118
- package/dist/astro/routes/api/mcp.mjs.map +1 -1
- package/dist/astro/routes/api/media/_id_/confirm.mjs +19 -19
- package/dist/astro/routes/api/media/_id_/replace.mjs +16 -16
- package/dist/astro/routes/api/media/_id_/upload.mjs +9 -9
- package/dist/astro/routes/api/media/_id_/usage.mjs +33 -33
- package/dist/astro/routes/api/media/_id_.mjs +31 -31
- package/dist/astro/routes/api/media/asset/_id_/_filename_.mjs +6 -6
- package/dist/astro/routes/api/media/file/_...key_.mjs +9 -9
- package/dist/astro/routes/api/media/folders/_id_.mjs +16 -16
- package/dist/astro/routes/api/media/folders/index.mjs +16 -16
- package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +5 -5
- package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +5 -5
- package/dist/astro/routes/api/media/providers/index.mjs +5 -5
- package/dist/astro/routes/api/media/upload-url.mjs +18 -18
- package/dist/astro/routes/api/media.mjs +35 -35
- package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +17 -17
- package/dist/astro/routes/api/menus/_name_/items.mjs +17 -17
- package/dist/astro/routes/api/menus/_name_/reorder.mjs +17 -17
- package/dist/astro/routes/api/menus/_name_/translations.mjs +17 -17
- package/dist/astro/routes/api/menus/_name_.mjs +17 -17
- package/dist/astro/routes/api/menus/index.mjs +17 -17
- package/dist/astro/routes/api/oauth/authorize.mjs +6 -6
- package/dist/astro/routes/api/oauth/device/authorize.d.mts +2 -1
- package/dist/astro/routes/api/oauth/device/authorize.d.mts.map +1 -1
- package/dist/astro/routes/api/oauth/device/authorize.mjs +23 -9
- package/dist/astro/routes/api/oauth/device/authorize.mjs.map +1 -1
- package/dist/astro/routes/api/oauth/device/code.mjs +11 -11
- package/dist/astro/routes/api/oauth/device/token.mjs +10 -10
- package/dist/astro/routes/api/oauth/register.mjs +5 -5
- package/dist/astro/routes/api/oauth/token/refresh.mjs +8 -8
- package/dist/astro/routes/api/oauth/token/revoke.mjs +8 -8
- package/dist/astro/routes/api/oauth/token.mjs +8 -8
- package/dist/astro/routes/api/openapi.json.mjs +20 -20
- package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +11 -11
- package/dist/astro/routes/api/redirects/404s/index.mjs +19 -19
- package/dist/astro/routes/api/redirects/404s/summary.mjs +19 -19
- package/dist/astro/routes/api/redirects/_id_.mjs +19 -19
- package/dist/astro/routes/api/redirects/index.mjs +19 -19
- package/dist/astro/routes/api/relations/_id_/index.mjs +34 -29
- package/dist/astro/routes/api/relations/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/relations/index.mjs +36 -31
- package/dist/astro/routes/api/relations/index.mjs.map +1 -1
- package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +5 -5
- package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +5 -5
- package/dist/astro/routes/api/schema/block-types/_slug_/index.mjs +71 -67
- package/dist/astro/routes/api/schema/block-types/_slug_/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/block-types/_slug_/versions/_version_/activate.mjs +71 -67
- package/dist/astro/routes/api/schema/block-types/_slug_/versions/_version_/activate.mjs.map +1 -1
- package/dist/astro/routes/api/schema/block-types/index.mjs +71 -67
- package/dist/astro/routes/api/schema/block-types/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +73 -69
- 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 +71 -67
- 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 +71 -67
- package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +71 -67
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/index.mjs +71 -67
- package/dist/astro/routes/api/schema/collections/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/reorder.mjs +71 -67
- package/dist/astro/routes/api/schema/collections/reorder.mjs.map +1 -1
- package/dist/astro/routes/api/schema/index.mjs +16 -16
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs +71 -67
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs.map +1 -1
- package/dist/astro/routes/api/schema/orphans/index.mjs +71 -67
- package/dist/astro/routes/api/schema/orphans/index.mjs.map +1 -1
- package/dist/astro/routes/api/search/enable.mjs +19 -19
- package/dist/astro/routes/api/search/index.mjs +18 -18
- package/dist/astro/routes/api/search/rebuild.mjs +19 -19
- package/dist/astro/routes/api/search/stats.mjs +9 -9
- package/dist/astro/routes/api/search/suggest.mjs +18 -18
- package/dist/astro/routes/api/sections/_slug_.mjs +18 -18
- package/dist/astro/routes/api/sections/index.mjs +18 -18
- package/dist/astro/routes/api/settings/backups/archives/_name_.mjs +7 -7
- package/dist/astro/routes/api/settings/backups/archives/index.mjs +7 -7
- package/dist/astro/routes/api/settings/backups/export.mjs +7 -7
- package/dist/astro/routes/api/settings/backups/index.mjs +8 -8
- package/dist/astro/routes/api/settings/email.mjs +6 -6
- package/dist/astro/routes/api/settings.mjs +21 -21
- package/dist/astro/routes/api/setup/admin-verify.mjs +19 -19
- package/dist/astro/routes/api/setup/admin.mjs +18 -18
- package/dist/astro/routes/api/setup/dev-bypass.mjs +45 -39
- 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.d.mts.map +1 -1
- package/dist/astro/routes/api/setup/index.mjs +80 -49
- package/dist/astro/routes/api/setup/index.mjs.map +1 -1
- package/dist/astro/routes/api/setup/status.mjs +6 -6
- package/dist/astro/routes/api/snapshot.mjs +9 -9
- package/dist/astro/routes/api/snapshot.mjs.map +1 -1
- package/dist/astro/routes/api/taxonomies/_name_/reorder.mjs +23 -23
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +23 -23
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +23 -23
- package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +23 -23
- package/dist/astro/routes/api/taxonomies/_name_/translations.mjs +22 -22
- package/dist/astro/routes/api/taxonomies/_name_.mjs +23 -23
- package/dist/astro/routes/api/taxonomies/bulk-tag.mjs +36 -35
- package/dist/astro/routes/api/taxonomies/bulk-tag.mjs.map +1 -1
- package/dist/astro/routes/api/taxonomies/index.mjs +23 -23
- package/dist/astro/routes/api/themes/preview.mjs +6 -6
- package/dist/astro/routes/api/typegen.mjs +8 -8
- package/dist/astro/routes/api/visual-editing/action-token.mjs +6 -6
- package/dist/astro/routes/api/visual-editing/content/_collection_/_id_/publish.mjs +19 -19
- package/dist/astro/routes/api/visual-editing/toolbar-labels.mjs +4 -4
- package/dist/astro/routes/api/well-known/auth.mjs +1 -1
- 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 +16 -16
- package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +19 -19
- package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +19 -19
- package/dist/astro/routes/api/widget-areas/_name_.mjs +9 -9
- package/dist/astro/routes/api/widget-areas/index.mjs +19 -19
- package/dist/astro/routes/api/widget-components.mjs +5 -5
- package/dist/astro/routes/robots.txt.mjs +6 -6
- package/dist/astro/routes/sitemap-_collection_.xml.mjs +9 -9
- package/dist/astro/routes/sitemap.xml.mjs +7 -7
- package/dist/astro/types.d.mts +28 -18
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/audit-CrgmE9LE.mjs +127 -0
- package/dist/audit-CrgmE9LE.mjs.map +1 -0
- package/dist/auth/providers/github.d.mts +1 -1
- package/dist/auth/providers/google.d.mts +1 -1
- package/dist/authoritative-records-C2YzIdbu.mjs +5172 -0
- package/dist/authoritative-records-C2YzIdbu.mjs.map +1 -0
- package/dist/authorize-WgdKF8QY.mjs +49 -0
- package/dist/authorize-WgdKF8QY.mjs.map +1 -0
- package/dist/backup-Ck8_lMO3.mjs +246 -0
- package/dist/backup-Ck8_lMO3.mjs.map +1 -0
- package/dist/block-types-BQ90asVI.mjs +132 -0
- package/dist/block-types-BQ90asVI.mjs.map +1 -0
- package/dist/block-values-ACFqgB1D.mjs +175 -0
- package/dist/block-values-ACFqgB1D.mjs.map +1 -0
- package/dist/bundle-Db1iNMv_.mjs +667 -0
- package/dist/bundle-Db1iNMv_.mjs.map +1 -0
- package/dist/byline-BayaYGoj.mjs +909 -0
- package/dist/byline-BayaYGoj.mjs.map +1 -0
- package/dist/byline-fields-B8HH-35f.mjs +254 -0
- package/dist/byline-fields-B8HH-35f.mjs.map +1 -0
- package/dist/byline-fields-hMPjf6Eu.mjs +123 -0
- package/dist/byline-fields-hMPjf6Eu.mjs.map +1 -0
- package/dist/byline-registry-B_EK_TQf.mjs +406 -0
- package/dist/byline-registry-B_EK_TQf.mjs.map +1 -0
- package/dist/bylines-DFAaFXWi.mjs +214 -0
- package/dist/bylines-DFAaFXWi.mjs.map +1 -0
- package/dist/bylines-DkIxUxmr.mjs +232 -0
- package/dist/bylines-DkIxUxmr.mjs.map +1 -0
- package/dist/cache-DkvhQ8XE.mjs +108 -0
- package/dist/cache-DkvhQ8XE.mjs.map +1 -0
- package/dist/canonical-CcSuuTlH.mjs +174 -0
- package/dist/canonical-CcSuuTlH.mjs.map +1 -0
- package/dist/capture-triggers-DJEoOTOq.mjs +362 -0
- package/dist/capture-triggers-DJEoOTOq.mjs.map +1 -0
- package/dist/challenge-store-CvBl81AQ.mjs +51 -0
- package/dist/challenge-store-CvBl81AQ.mjs.map +1 -0
- package/dist/checksum-CClweQzI.mjs +162 -0
- package/dist/checksum-CClweQzI.mjs.map +1 -0
- package/dist/chrome-tags-D5caJwwp.mjs +24 -0
- package/dist/chrome-tags-D5caJwwp.mjs.map +1 -0
- package/dist/chunks-DwBaj8aT.mjs +25 -0
- package/dist/chunks-DwBaj8aT.mjs.map +1 -0
- package/dist/cli/index.mjs +374 -761
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/cf-access.d.mts +1 -1
- package/dist/client/index.d.mts +18 -18
- package/dist/client/index.mjs +2 -2
- package/dist/collection-deletion-CLCdXsnV.mjs +436 -0
- package/dist/collection-deletion-CLCdXsnV.mjs.map +1 -0
- package/dist/collection-deletion-processor-hNw8x01F.mjs +217 -0
- package/dist/collection-deletion-processor-hNw8x01F.mjs.map +1 -0
- package/dist/collection-slugs-cache-CUuiduLq.mjs +85 -0
- package/dist/collection-slugs-cache-CUuiduLq.mjs.map +1 -0
- package/dist/comment-D2uvL63j.mjs +313 -0
- package/dist/comment-D2uvL63j.mjs.map +1 -0
- package/dist/comment-reaction-DevUuzg4.mjs +85 -0
- package/dist/comment-reaction-DevUuzg4.mjs.map +1 -0
- package/dist/comments-Cq0s7_hO.mjs +195 -0
- package/dist/comments-Cq0s7_hO.mjs.map +1 -0
- package/dist/comments-M4EDYAHw.mjs +99 -0
- package/dist/comments-M4EDYAHw.mjs.map +1 -0
- package/dist/components-CuIGwXMs.mjs +108 -0
- package/dist/components-CuIGwXMs.mjs.map +1 -0
- package/dist/config-52Ea5IWt.d.mts +39 -0
- package/dist/config-52Ea5IWt.d.mts.map +1 -0
- package/dist/config-CCz3xOLc.mjs +242 -0
- package/dist/config-CCz3xOLc.mjs.map +1 -0
- package/dist/config-D3d5Qudq.d.mts +6669 -0
- package/dist/config-D3d5Qudq.d.mts.map +1 -0
- package/dist/content-CbnufDsM.mjs +2018 -0
- package/dist/content-CbnufDsM.mjs.map +1 -0
- package/dist/content-list-query-D_7B1FE5.d.mts +26 -0
- package/dist/content-list-query-D_7B1FE5.d.mts.map +1 -0
- package/dist/content-policy-CYu6K1KB.mjs +46 -0
- package/dist/content-policy-CYu6K1KB.mjs.map +1 -0
- package/dist/content-repair-DlDUgFGp.mjs +442 -0
- package/dist/content-repair-DlDUgFGp.mjs.map +1 -0
- package/dist/context-CGQLA1-a.mjs +1922 -0
- package/dist/context-CGQLA1-a.mjs.map +1 -0
- package/dist/cron-c4D5C5Fx.mjs +284 -0
- package/dist/cron-c4D5C5Fx.mjs.map +1 -0
- package/dist/dashboard-CBq9kmOn.mjs +151 -0
- package/dist/dashboard-CBq9kmOn.mjs.map +1 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +1 -1
- package/dist/db/libsql-migrations.d.mts +1 -1
- package/dist/db/libsql-migrations.mjs +4 -4
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/postgres-migrations.d.mts +1 -1
- package/dist/db/postgres-migrations.mjs +4 -4
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite-migrations.d.mts +1 -1
- package/dist/db/sqlite-migrations.mjs +4 -4
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/db/sqlite.mjs +1 -1
- package/dist/default-DPsys51U.mjs +81 -0
- package/dist/default-DPsys51U.mjs.map +1 -0
- package/dist/deferred-tasks-CZ7rzBdo.d.mts +9 -0
- package/dist/deferred-tasks-CZ7rzBdo.d.mts.map +1 -0
- package/dist/dev-typegen-HRxwLRcT.mjs +29 -0
- package/dist/dev-typegen-HRxwLRcT.mjs.map +1 -0
- package/dist/device-flow-OcSqE9uY.mjs +521 -0
- package/dist/device-flow-OcSqE9uY.mjs.map +1 -0
- package/dist/digest-DVZ--cE9.mjs +194 -0
- package/dist/digest-DVZ--cE9.mjs.map +1 -0
- package/dist/direct-executor-DyjAqkpT.mjs +104 -0
- package/dist/direct-executor-DyjAqkpT.mjs.map +1 -0
- package/dist/dist-FPR93_xf.mjs +810 -0
- package/dist/dist-FPR93_xf.mjs.map +1 -0
- package/dist/email-console-DyQIgEnz.mjs +50 -0
- package/dist/email-console-DyQIgEnz.mjs.map +1 -0
- package/dist/email-locale-BYb16rVd.mjs +25 -0
- package/dist/email-locale-BYb16rVd.mjs.map +1 -0
- package/dist/emdash-runtime-CrzxJFmF.mjs +4214 -0
- package/dist/emdash-runtime-CrzxJFmF.mjs.map +1 -0
- package/dist/enrich-DBtCncE2.mjs +194 -0
- package/dist/enrich-DBtCncE2.mjs.map +1 -0
- package/dist/entry-lock-BN5y4YbO.d.mts +4481 -0
- package/dist/entry-lock-BN5y4YbO.d.mts.map +1 -0
- package/dist/entry-lock-BaRBHCsH.mjs +1173 -0
- package/dist/entry-lock-BaRBHCsH.mjs.map +1 -0
- package/dist/entry-lock-C6NcnZPV.mjs +163 -0
- package/dist/entry-lock-C6NcnZPV.mjs.map +1 -0
- package/dist/entry-locks-BTyQLdbC.mjs +167 -0
- package/dist/entry-locks-BTyQLdbC.mjs.map +1 -0
- package/dist/error-BfsNNrIE.mjs +90 -0
- package/dist/error-BfsNNrIE.mjs.map +1 -0
- package/dist/errors-BN1QAnEY.mjs +460 -0
- package/dist/errors-BN1QAnEY.mjs.map +1 -0
- package/dist/errors-Bt3goF8I.mjs +120 -0
- package/dist/errors-Bt3goF8I.mjs.map +1 -0
- package/dist/escape-BveidMa2.mjs +9 -0
- package/dist/escape-BveidMa2.mjs.map +1 -0
- package/dist/fence-Bb_IbCxI.mjs +162 -0
- package/dist/fence-Bb_IbCxI.mjs.map +1 -0
- package/dist/field-defs-cache-CsZctvbu.mjs +77 -0
- package/dist/field-defs-cache-CsZctvbu.mjs.map +1 -0
- package/dist/field-map-6m9rF49t.mjs +83 -0
- package/dist/field-map-6m9rF49t.mjs.map +1 -0
- package/dist/focal-point-CZ3d1wK5.mjs +23 -0
- package/dist/focal-point-CZ3d1wK5.mjs.map +1 -0
- package/dist/fts-manager-CCaZTp8y.mjs +443 -0
- package/dist/fts-manager-CCaZTp8y.mjs.map +1 -0
- package/dist/gc-xowRjczI.mjs +75 -0
- package/dist/gc-xowRjczI.mjs.map +1 -0
- package/dist/hash-COVK27Pc.mjs +37 -0
- package/dist/hash-COVK27Pc.mjs.map +1 -0
- package/dist/hook-result-CYSk0XFe.d.mts +31 -0
- package/dist/hook-result-CYSk0XFe.d.mts.map +1 -0
- package/dist/hreflang-CChPV7jx.mjs +105 -0
- package/dist/hreflang-CChPV7jx.mjs.map +1 -0
- package/dist/hreflang-DRslq3QI.d.mts +50 -0
- package/dist/hreflang-DRslq3QI.d.mts.map +1 -0
- package/dist/http-route-dispatch-CAdp2onn.mjs +339 -0
- package/dist/http-route-dispatch-CAdp2onn.mjs.map +1 -0
- package/dist/identity-NVTOOQyq.mjs +35 -0
- package/dist/identity-NVTOOQyq.mjs.map +1 -0
- package/dist/import-wpSO2N0x.mjs +2020 -0
- package/dist/import-wpSO2N0x.mjs.map +1 -0
- package/dist/index-CDs2wq4X.d.mts +623 -0
- package/dist/index-CDs2wq4X.d.mts.map +1 -0
- package/dist/index.d.mts +24 -24
- package/dist/index.mjs +103 -101
- package/dist/install-finalization-D9R9R-W8.mjs +58 -0
- package/dist/install-finalization-D9R9R-W8.mjs.map +1 -0
- package/dist/keys-Bck6LBfF.mjs +61 -0
- package/dist/keys-Bck6LBfF.mjs.map +1 -0
- package/dist/kinds-CYmRESWj.mjs +974 -0
- package/dist/kinds-CYmRESWj.mjs.map +1 -0
- package/dist/lifecycle-C815ZQfV.mjs +25 -0
- package/dist/lifecycle-C815ZQfV.mjs.map +1 -0
- package/dist/load-D99axixL.mjs +28 -0
- package/dist/load-D99axixL.mjs.map +1 -0
- package/dist/loader-CrRPfkHS.mjs +1197 -0
- package/dist/loader-CrRPfkHS.mjs.map +1 -0
- package/dist/loops-DMKlygqX.mjs +203 -0
- package/dist/loops-DMKlygqX.mjs.map +1 -0
- package/dist/manifest-builder-BQzCHNgB.mjs +189 -0
- package/dist/manifest-builder-BQzCHNgB.mjs.map +1 -0
- package/dist/manifest-schema-Ci2gmd0u.mjs +492 -0
- package/dist/manifest-schema-Ci2gmd0u.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 -14
- package/dist/media/local-runtime.mjs +6 -6
- package/dist/media-BUQePMxP.mjs +373 -0
- package/dist/media-BUQePMxP.mjs.map +1 -0
- package/dist/media-DH8ibNlY.mjs +294 -0
- package/dist/media-DH8ibNlY.mjs.map +1 -0
- package/dist/media-OEIQ3bVc.mjs +408 -0
- package/dist/media-OEIQ3bVc.mjs.map +1 -0
- package/dist/media-allowlist-pYtaBA95.mjs +44 -0
- package/dist/media-allowlist-pYtaBA95.mjs.map +1 -0
- package/dist/media-folders-Bf8_s4de.mjs +210 -0
- package/dist/media-folders-Bf8_s4de.mjs.map +1 -0
- package/dist/media-url-C6klEuNd.mjs +43 -0
- package/dist/media-url-C6klEuNd.mjs.map +1 -0
- package/dist/media-usage-D7Y64m6v.mjs +1629 -0
- package/dist/media-usage-D7Y64m6v.mjs.map +1 -0
- package/dist/media-usage-activation-DrqbmgLA.mjs +78 -0
- package/dist/media-usage-activation-DrqbmgLA.mjs.map +1 -0
- package/dist/media-usage-work-DommYhYA.mjs +152 -0
- package/dist/media-usage-work-DommYhYA.mjs.map +1 -0
- package/dist/media-usage-work-HU63hNDu.mjs +586 -0
- package/dist/media-usage-work-HU63hNDu.mjs.map +1 -0
- package/dist/menus-BQfusWEC.mjs +3608 -0
- package/dist/menus-BQfusWEC.mjs.map +1 -0
- package/dist/menus-BtwjQt5s.mjs +753 -0
- package/dist/menus-BtwjQt5s.mjs.map +1 -0
- package/dist/menus-p_M1V7J_.mjs +139 -0
- package/dist/menus-p_M1V7J_.mjs.map +1 -0
- package/dist/migrations/index.d.mts +1 -1
- package/dist/migrations/index.mjs +3 -3
- package/dist/mime-C94qBG-P.mjs +36 -0
- package/dist/mime-C94qBG-P.mjs.map +1 -0
- package/dist/mode-DpYW3w6U.mjs +23 -0
- package/dist/mode-DpYW3w6U.mjs.map +1 -0
- package/dist/node-sqlite-compat-pWFw0lcc.mjs +46 -0
- package/dist/node-sqlite-compat-pWFw0lcc.mjs.map +1 -0
- package/dist/normalize-DduyxBjf.mjs +191 -0
- package/dist/normalize-DduyxBjf.mjs.map +1 -0
- package/dist/oauth-authorization-DAXsH1H9.mjs +274 -0
- package/dist/oauth-authorization-DAXsH1H9.mjs.map +1 -0
- package/dist/oauth-clients-DENiDQri.mjs +266 -0
- package/dist/oauth-clients-DENiDQri.mjs.map +1 -0
- package/dist/oauth-state-store-y8mEYVq9.mjs +50 -0
- package/dist/oauth-state-store-y8mEYVq9.mjs.map +1 -0
- package/dist/oauth-user-lookup-CtTYAy-O.mjs +26 -0
- package/dist/oauth-user-lookup-CtTYAy-O.mjs.map +1 -0
- package/dist/object-cache/memory.d.mts +1 -1
- package/dist/options-BYa1FgIM.d.mts +81 -0
- package/dist/options-BYa1FgIM.d.mts.map +1 -0
- package/dist/page/index.d.mts +2 -2
- package/dist/page/index.mjs +5 -5
- package/dist/parse-CORGGhHf.mjs +81 -0
- package/dist/parse-CORGGhHf.mjs.map +1 -0
- package/dist/parse-avNM1xWV.d.mts +168 -0
- package/dist/parse-avNM1xWV.d.mts.map +1 -0
- package/dist/passkey-config-Dih5YeIS.mjs +46 -0
- package/dist/passkey-config-Dih5YeIS.mjs.map +1 -0
- package/dist/paths-d51RFm8M.mjs +77 -0
- package/dist/paths-d51RFm8M.mjs.map +1 -0
- package/dist/patterns-CoqlXUvk.mjs +145 -0
- package/dist/patterns-CoqlXUvk.mjs.map +1 -0
- package/dist/placeholder-D2aksPMF.d.mts +306 -0
- package/dist/placeholder-D2aksPMF.d.mts.map +1 -0
- package/dist/plan-DqeZKuaR.mjs +135 -0
- package/dist/plan-DqeZKuaR.mjs.map +1 -0
- package/dist/plugin-test-runtime.d.mts +14 -14
- package/dist/plugin-test-runtime.mjs +110 -107
- package/dist/plugin-types.d.mts +2 -2
- package/dist/plugin-utils.d.mts +12 -12
- package/dist/plugins/adapt-sandbox-entry.d.mts +12 -12
- package/dist/plugins/adapt-sandbox-entry.mjs +3 -3
- package/dist/plugins/host.d.mts +5 -5
- package/dist/plugins/host.mjs +42 -41
- package/dist/plugins-Bb7oqjQA.mjs +277 -0
- package/dist/plugins-Bb7oqjQA.mjs.map +1 -0
- package/dist/policy-BcEqut3n.mjs +45 -0
- package/dist/policy-BcEqut3n.mjs.map +1 -0
- package/dist/portable-text-DXE8ymtD.mjs +307 -0
- package/dist/portable-text-DXE8ymtD.mjs.map +1 -0
- package/dist/preview-DEDWbwA_.mjs +107 -0
- package/dist/preview-DEDWbwA_.mjs.map +1 -0
- package/dist/protocol-BJbTzvwl.d.mts +61 -0
- package/dist/protocol-BJbTzvwl.d.mts.map +1 -0
- package/dist/public-submission-D9c9S14m.mjs +250 -0
- package/dist/public-submission-D9c9S14m.mjs.map +1 -0
- package/dist/public-url-CIRe-2T6.mjs +101 -0
- package/dist/public-url-CIRe-2T6.mjs.map +1 -0
- package/dist/query-BzifXlPS.mjs +1156 -0
- package/dist/query-BzifXlPS.mjs.map +1 -0
- package/dist/query-OzMJ6vJP.mjs +41 -0
- package/dist/query-OzMJ6vJP.mjs.map +1 -0
- package/dist/rate-limit-Csp60Vik.mjs +120 -0
- package/dist/rate-limit-Csp60Vik.mjs.map +1 -0
- package/dist/receipt-B-aU4hpJ.mjs +43 -0
- package/dist/receipt-B-aU4hpJ.mjs.map +1 -0
- package/dist/redirect-CDSw1sU3.mjs +17 -0
- package/dist/redirect-CDSw1sU3.mjs.map +1 -0
- package/dist/redirect-D1ArbO85.mjs +411 -0
- package/dist/redirect-D1ArbO85.mjs.map +1 -0
- package/dist/redirects-BjTwDTsV.mjs +148 -0
- package/dist/redirects-BjTwDTsV.mjs.map +1 -0
- package/dist/redirects-Ck50kH3n.mjs +446 -0
- package/dist/redirects-Ck50kH3n.mjs.map +1 -0
- package/dist/registry/testing.mjs +3 -3
- package/dist/registry-CFcNCayZ.mjs +6754 -0
- package/dist/registry-CFcNCayZ.mjs.map +1 -0
- package/dist/relations-Cfaa1fDl.mjs +776 -0
- package/dist/relations-Cfaa1fDl.mjs.map +1 -0
- package/dist/request-cache-iHRlHx1d.mjs +104 -0
- package/dist/request-cache-iHRlHx1d.mjs.map +1 -0
- package/dist/request-context.d.mts +1 -1
- package/dist/request-meta-D47mqyev.mjs +141 -0
- package/dist/request-meta-D47mqyev.mjs.map +1 -0
- package/dist/resolve-BPE8sMvx.mjs +99 -0
- package/dist/resolve-BPE8sMvx.mjs.map +1 -0
- package/dist/resolve-C0s1E_fH.mjs +199 -0
- package/dist/resolve-C0s1E_fH.mjs.map +1 -0
- package/dist/revision-j1p4-bOj.mjs +302 -0
- package/dist/revision-j1p4-bOj.mjs.map +1 -0
- package/dist/route-wire-C9PJV9aL.mjs +197 -0
- package/dist/route-wire-C9PJV9aL.mjs.map +1 -0
- package/dist/routes-lyYw0mMf.mjs +272 -0
- package/dist/routes-lyYw0mMf.mjs.map +1 -0
- package/dist/runner-B3eyB9Gb.mjs +6121 -0
- package/dist/runner-B3eyB9Gb.mjs.map +1 -0
- package/dist/runner-DoYH2Jcu.d.mts +68 -0
- package/dist/runner-DoYH2Jcu.d.mts.map +1 -0
- package/dist/runtime.d.mts +13 -13
- package/dist/runtime.mjs +3 -3
- package/dist/schema-BAayK46S.mjs +13 -0
- package/dist/schema-BAayK46S.mjs.map +1 -0
- package/dist/schema-Gqcl-8G4.mjs +815 -0
- package/dist/schema-Gqcl-8G4.mjs.map +1 -0
- package/dist/scopes-BqPOKoKf.mjs +34 -0
- package/dist/scopes-BqPOKoKf.mjs.map +1 -0
- package/dist/search-YJKPIEV6.mjs +448 -0
- package/dist/search-YJKPIEV6.mjs.map +1 -0
- package/dist/sections-BR_e-wK5.mjs +346 -0
- package/dist/sections-BR_e-wK5.mjs.map +1 -0
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +37 -31
- package/dist/seo/index.d.mts +3 -3
- package/dist/seo/index.mjs +22 -21
- package/dist/seo/index.mjs.map +1 -1
- package/dist/seo-Dsj8hy3j.mjs +114 -0
- package/dist/seo-Dsj8hy3j.mjs.map +1 -0
- package/dist/seo-XvH4LNNp.mjs +133 -0
- package/dist/seo-XvH4LNNp.mjs.map +1 -0
- package/dist/service-BsVG05gS.mjs +150 -0
- package/dist/service-BsVG05gS.mjs.map +1 -0
- package/dist/session-user-DgKbYrlj.mjs +51 -0
- package/dist/session-user-DgKbYrlj.mjs.map +1 -0
- package/dist/settings-CNPUntkh.mjs +51 -0
- package/dist/settings-CNPUntkh.mjs.map +1 -0
- package/dist/settings-D5EHi0Tk.mjs +439 -0
- package/dist/settings-D5EHi0Tk.mjs.map +1 -0
- package/dist/settings-DSfE7wOa.mjs +61 -0
- package/dist/settings-DSfE7wOa.mjs.map +1 -0
- package/dist/setup-complete-B8W8sJ9v.mjs +26 -0
- package/dist/setup-complete-B8W8sJ9v.mjs.map +1 -0
- package/dist/setup-nonce-DnSBP260.mjs +25 -0
- package/dist/setup-nonce-DnSBP260.mjs.map +1 -0
- package/dist/site-settings-NGzdrt7O.mjs +50 -0
- package/dist/site-settings-NGzdrt7O.mjs.map +1 -0
- package/dist/site-url-D6JB7tLW.mjs +16 -0
- package/dist/site-url-D6JB7tLW.mjs.map +1 -0
- package/dist/snapshot-CdcwwWAf.mjs +414 -0
- package/dist/snapshot-CdcwwWAf.mjs.map +1 -0
- package/dist/ssrf-Dv0_tRpo.mjs +406 -0
- package/dist/ssrf-Dv0_tRpo.mjs.map +1 -0
- package/dist/stage-CUjhED3P.mjs +662 -0
- package/dist/stage-CUjhED3P.mjs.map +1 -0
- package/dist/staged-C36IWXff.mjs +659 -0
- package/dist/staged-C36IWXff.mjs.map +1 -0
- package/dist/state-CKouMCSY.mjs +153 -0
- package/dist/state-CKouMCSY.mjs.map +1 -0
- package/dist/status-lzyj4Uu7.mjs +30 -0
- package/dist/status-lzyj4Uu7.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-B-cWcTjJ.d.mts +1594 -0
- package/dist/storage-query-B-cWcTjJ.d.mts.map +1 -0
- package/dist/tar-DsTXX6ch.mjs +449 -0
- package/dist/tar-DsTXX6ch.mjs.map +1 -0
- package/dist/target-DLQCwp_Z.mjs +27 -0
- package/dist/target-DLQCwp_Z.mjs.map +1 -0
- package/dist/taxonomies-BlAjr4BL.mjs +697 -0
- package/dist/taxonomies-BlAjr4BL.mjs.map +1 -0
- package/dist/taxonomies-Byyo7LOZ.mjs +939 -0
- package/dist/taxonomies-Byyo7LOZ.mjs.map +1 -0
- package/dist/taxonomy-CKDwM61Y.mjs +525 -0
- package/dist/taxonomy-CKDwM61Y.mjs.map +1 -0
- package/dist/tokens-BojXlk49.mjs +171 -0
- package/dist/tokens-BojXlk49.mjs.map +1 -0
- package/dist/transaction-DLPQWik1.mjs +29 -0
- package/dist/transaction-DLPQWik1.mjs.map +1 -0
- package/dist/transfer-XSTh2BCr.mjs +8706 -0
- package/dist/transfer-XSTh2BCr.mjs.map +1 -0
- package/dist/transfer-download-DkoCDA9d.mjs +33 -0
- package/dist/transfer-download-DkoCDA9d.mjs.map +1 -0
- package/dist/transfer-iQ26J-RT.mjs +166 -0
- package/dist/transfer-iQ26J-RT.mjs.map +1 -0
- package/dist/transformations-C7RQabKQ.mjs +196 -0
- package/dist/transformations-C7RQabKQ.mjs.map +1 -0
- package/dist/transport-D3RulfD6.mjs +136 -0
- package/dist/transport-D3RulfD6.mjs.map +1 -0
- package/dist/transport-DhlZlf4D.d.mts +42 -0
- package/dist/transport-DhlZlf4D.d.mts.map +1 -0
- package/dist/trusted-proxy-B6ECOZJc.mjs +51 -0
- package/dist/trusted-proxy-B6ECOZJc.mjs.map +1 -0
- package/dist/turnstile-lqUiY7lP.mjs +53 -0
- package/dist/turnstile-lqUiY7lP.mjs.map +1 -0
- package/dist/types-4Dn9bZhu.mjs +77 -0
- package/dist/types-4Dn9bZhu.mjs.map +1 -0
- package/dist/types-5LSo7PPx.d.mts +85 -0
- package/dist/types-5LSo7PPx.d.mts.map +1 -0
- package/dist/types-BQ3hwYJT.d.mts +896 -0
- package/dist/types-BQ3hwYJT.d.mts.map +1 -0
- package/dist/types-BYEk7qo3.d.mts +198 -0
- package/dist/types-BYEk7qo3.d.mts.map +1 -0
- package/dist/types-CJKsWVFR.mjs +199 -0
- package/dist/types-CJKsWVFR.mjs.map +1 -0
- package/dist/types-Ca_w8mmY.d.mts +363 -0
- package/dist/types-Ca_w8mmY.d.mts.map +1 -0
- package/dist/types-CkE41ZAG.mjs +18 -0
- package/dist/types-CkE41ZAG.mjs.map +1 -0
- package/dist/types-D0aJW2uR.d.mts +1641 -0
- package/dist/types-D0aJW2uR.d.mts.map +1 -0
- package/dist/types-DbpN4Qto.d.mts +372 -0
- package/dist/types-DbpN4Qto.d.mts.map +1 -0
- package/dist/types-ILDI1sdY.mjs +16 -0
- package/dist/types-ILDI1sdY.mjs.map +1 -0
- package/dist/types-OC5c-edM.mjs +22 -0
- package/dist/types-OC5c-edM.mjs.map +1 -0
- package/dist/types-aOOxRSx6.mjs +103 -0
- package/dist/types-aOOxRSx6.mjs.map +1 -0
- package/dist/types-m1WbUffD.d.mts +198 -0
- package/dist/types-m1WbUffD.d.mts.map +1 -0
- package/dist/types-uxLoPBio.d.mts +124 -0
- package/dist/types-uxLoPBio.d.mts.map +1 -0
- package/dist/types-we5uR-tV.d.mts +287 -0
- package/dist/types-we5uR-tV.d.mts.map +1 -0
- package/dist/upload-attempts-DduMy6B0.mjs +32 -0
- package/dist/upload-attempts-DduMy6B0.mjs.map +1 -0
- package/dist/url-pattern-B3EwxAAk.mjs +33 -0
- package/dist/url-pattern-B3EwxAAk.mjs.map +1 -0
- package/dist/user-ElwQjTQT.mjs +171 -0
- package/dist/user-ElwQjTQT.mjs.map +1 -0
- package/dist/utils-BlAXmfOG.mjs +439 -0
- package/dist/utils-BlAXmfOG.mjs.map +1 -0
- package/dist/validate-CH0X5b2c.d.mts +468 -0
- package/dist/validate-CH0X5b2c.d.mts.map +1 -0
- package/dist/validate-CfgRpcjA.mjs +492 -0
- package/dist/validate-CfgRpcjA.mjs.map +1 -0
- package/dist/validation-DvYJtv4k.mjs +218 -0
- package/dist/validation-DvYJtv4k.mjs.map +1 -0
- package/dist/version-DgYjhTud.mjs +9 -0
- package/dist/version-DgYjhTud.mjs.map +1 -0
- package/dist/version-Dq71scUb.mjs +7 -0
- package/dist/version-Dq71scUb.mjs.map +1 -0
- package/dist/widgets-ChFIj0be.mjs +126 -0
- package/dist/widgets-ChFIj0be.mjs.map +1 -0
- package/dist/wxr-taxonomies-CBZMp2YX.mjs +372 -0
- package/dist/wxr-taxonomies-CBZMp2YX.mjs.map +1 -0
- package/dist/wxr-taxonomies-DlKDpWN1.d.mts +34 -0
- package/dist/wxr-taxonomies-DlKDpWN1.d.mts.map +1 -0
- package/dist/zod-generator-DmTKI-Ig.mjs +426 -0
- package/dist/zod-generator-DmTKI-Ig.mjs.map +1 -0
- package/package.json +7 -6
- package/src/api/handlers/content.ts +593 -16
- package/src/api/handlers/device-flow.ts +74 -21
- package/src/api/handlers/index.ts +1 -0
- package/src/api/handlers/manifest.ts +79 -5
- package/src/api/handlers/media-upload.ts +17 -71
- package/src/api/handlers/media.ts +97 -3
- package/src/api/handlers/relations.ts +638 -157
- package/src/api/handlers/revision.ts +30 -0
- package/src/api/handlers/schema.ts +403 -1
- package/src/api/handlers/snapshot.ts +257 -3
- package/src/api/handlers/staged-references.ts +142 -0
- package/src/api/handlers/validate-references.ts +146 -0
- package/src/api/schemas/content.ts +13 -0
- package/src/api/schemas/relations.ts +54 -44
- package/src/api/schemas/schema.ts +14 -0
- package/src/astro/integration/routes.ts +15 -19
- package/src/astro/integration/vite-config.ts +10 -4
- package/src/astro/middleware.ts +1 -0
- package/src/astro/routes/api/content/[collection]/[id]/references/[relation]/children.ts +11 -54
- package/src/astro/routes/api/content/[collection]/[id].ts +3 -1
- package/src/astro/routes/api/oauth/device/authorize.ts +33 -4
- package/src/astro/routes/api/relations/index.ts +4 -4
- package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +6 -2
- package/src/astro/routes/api/setup/index.ts +66 -23
- package/src/astro/routes/api/snapshot.ts +2 -1
- package/src/astro/types.ts +10 -0
- package/src/cleanup.ts +10 -0
- package/src/cli/commands/export-seed.ts +510 -89
- package/src/components/Columns.astro +3 -1
- package/src/components/Cover.astro +3 -1
- package/src/components/WidgetArea.astro +3 -2
- package/src/database/migrations/086_relations_structural.ts +197 -0
- package/src/database/migrations/087_reference_field_relations.ts +407 -0
- package/src/database/migrations/runner.ts +4 -0
- package/src/database/reference-backfill.ts +141 -0
- package/src/database/repositories/content.ts +43 -8
- package/src/database/repositories/media.ts +40 -1
- package/src/database/repositories/redirect.ts +17 -34
- package/src/database/repositories/relation.ts +620 -208
- package/src/database/repositories/revision.ts +16 -0
- package/src/database/repositories/types.ts +38 -0
- package/src/database/transaction.ts +12 -1
- package/src/database/types.ts +14 -5
- package/src/emdash-runtime.ts +114 -11
- package/src/index.ts +11 -0
- package/src/loader.ts +169 -22
- package/src/mcp/server.ts +13 -48
- package/src/query.ts +455 -25
- package/src/references/field-map.ts +115 -0
- package/src/references/resolve.ts +160 -0
- package/src/references/staged.ts +117 -0
- package/src/references/types.ts +16 -0
- package/src/registry/artifact-fetch.ts +45 -271
- package/src/schema/registry.ts +99 -24
- package/src/schema/types.ts +58 -0
- package/src/schema/zod-generator.ts +67 -1
- package/src/seed/apply.ts +841 -103
- package/src/seed/types.ts +36 -2
- package/src/seed/validate.ts +78 -2
- package/src/transfer/analyze/unique-keys.ts +3 -12
- package/src/transfer/analyze/validate.ts +65 -39
- package/src/transfer/analyze/values.ts +12 -1
- package/src/transfer/export/readers.ts +12 -8
- package/src/transfer/format/columns.ts +26 -6
- package/src/transfer/format/kinds.ts +21 -5
- package/src/transfer/import/context.ts +8 -1
- package/src/transfer/ops/package-index.ts +1 -1
- package/dist/action-token-DYwe4Pk0.mjs +0 -26
- package/dist/action-token-DYwe4Pk0.mjs.map +0 -1
- package/dist/activation-LOkDM5WT.mjs +0 -382
- package/dist/activation-LOkDM5WT.mjs.map +0 -1
- package/dist/adapters-B64LbYcB.d.mts +0 -171
- package/dist/adapters-B64LbYcB.d.mts.map +0 -1
- package/dist/allowed-origins-CHB8oZNM.mjs +0 -116
- package/dist/allowed-origins-CHB8oZNM.mjs.map +0 -1
- package/dist/api-cFFZpyS0.mjs +0 -4718
- package/dist/api-cFFZpyS0.mjs.map +0 -1
- package/dist/api-tokens-s4i4aUp_.mjs +0 -166
- package/dist/api-tokens-s4i4aUp_.mjs.map +0 -1
- package/dist/apply-CoT603H3.mjs +0 -1163
- package/dist/apply-CoT603H3.mjs.map +0 -1
- package/dist/approvals-CqMfNxak.mjs +0 -460
- package/dist/approvals-CqMfNxak.mjs.map +0 -1
- package/dist/artifact-fetch-DZMOGZ3-.mjs +0 -818
- package/dist/artifact-fetch-DZMOGZ3-.mjs.map +0 -1
- package/dist/astro/routes/api/relations/_id_/translations.d.mts +0 -8
- package/dist/astro/routes/api/relations/_id_/translations.d.mts.map +0 -1
- package/dist/astro/routes/api/relations/_id_/translations.mjs +0 -47
- package/dist/astro/routes/api/relations/_id_/translations.mjs.map +0 -1
- package/dist/audit-RfCugiSB.mjs +0 -127
- package/dist/audit-RfCugiSB.mjs.map +0 -1
- package/dist/authoritative-records-CoJvBXAD.mjs +0 -5172
- package/dist/authoritative-records-CoJvBXAD.mjs.map +0 -1
- package/dist/authorize-seCifOMJ.mjs +0 -49
- package/dist/authorize-seCifOMJ.mjs.map +0 -1
- package/dist/backup-CBu0LrXf.mjs +0 -246
- package/dist/backup-CBu0LrXf.mjs.map +0 -1
- package/dist/block-type-registry-DUI_3KxT.mjs +0 -6709
- package/dist/block-type-registry-DUI_3KxT.mjs.map +0 -1
- package/dist/block-types-BmZmLmAD.mjs +0 -132
- package/dist/block-types-BmZmLmAD.mjs.map +0 -1
- package/dist/block-values-C_VIPr5t.mjs +0 -175
- package/dist/block-values-C_VIPr5t.mjs.map +0 -1
- package/dist/byline-DYmkXqvo.mjs +0 -909
- package/dist/byline-DYmkXqvo.mjs.map +0 -1
- package/dist/byline-fields-D28AT-Fh.mjs +0 -123
- package/dist/byline-fields-D28AT-Fh.mjs.map +0 -1
- package/dist/byline-registry-gnKscbQX.mjs +0 -406
- package/dist/byline-registry-gnKscbQX.mjs.map +0 -1
- package/dist/bylines-BPwJqoYH.mjs +0 -232
- package/dist/bylines-BPwJqoYH.mjs.map +0 -1
- package/dist/bylines-ukLaYpVZ.mjs +0 -214
- package/dist/bylines-ukLaYpVZ.mjs.map +0 -1
- package/dist/cache-m7UEkRFV.mjs +0 -108
- package/dist/cache-m7UEkRFV.mjs.map +0 -1
- package/dist/canonical-YhEQezjg.mjs +0 -174
- package/dist/canonical-YhEQezjg.mjs.map +0 -1
- package/dist/capture-triggers-kJyl_yyM.mjs +0 -362
- package/dist/capture-triggers-kJyl_yyM.mjs.map +0 -1
- package/dist/challenge-store-ppU6Uv13.mjs +0 -51
- package/dist/challenge-store-ppU6Uv13.mjs.map +0 -1
- package/dist/checksum-B2IINIlN.mjs +0 -162
- package/dist/checksum-B2IINIlN.mjs.map +0 -1
- package/dist/chrome-tags-DApCQSIk.mjs +0 -24
- package/dist/chrome-tags-DApCQSIk.mjs.map +0 -1
- package/dist/chunks-Dms7Uxbt.mjs +0 -25
- package/dist/chunks-Dms7Uxbt.mjs.map +0 -1
- package/dist/collection-deletion-BIm28mo3.mjs +0 -436
- package/dist/collection-deletion-BIm28mo3.mjs.map +0 -1
- package/dist/collection-deletion-processor-Br6rxs3u.mjs +0 -217
- package/dist/collection-deletion-processor-Br6rxs3u.mjs.map +0 -1
- package/dist/collection-slugs-cache-CjGk__zp.mjs +0 -85
- package/dist/collection-slugs-cache-CjGk__zp.mjs.map +0 -1
- package/dist/comment-CrBl22OF.mjs +0 -313
- package/dist/comment-CrBl22OF.mjs.map +0 -1
- package/dist/comment-reaction-JEQ2Fpd8.mjs +0 -85
- package/dist/comment-reaction-JEQ2Fpd8.mjs.map +0 -1
- package/dist/comments-2ah9du9y.mjs +0 -195
- package/dist/comments-2ah9du9y.mjs.map +0 -1
- package/dist/comments-fHZ8ON9O.mjs +0 -99
- package/dist/comments-fHZ8ON9O.mjs.map +0 -1
- package/dist/components-CXbq7CSM.mjs +0 -108
- package/dist/components-CXbq7CSM.mjs.map +0 -1
- package/dist/config-5ii2haYu.d.mts +0 -6528
- package/dist/config-5ii2haYu.d.mts.map +0 -1
- package/dist/config-IhybvkIQ.mjs +0 -242
- package/dist/config-IhybvkIQ.mjs.map +0 -1
- package/dist/config-Py43JK9r.d.mts +0 -39
- package/dist/config-Py43JK9r.d.mts.map +0 -1
- package/dist/content-DFaIEDj9.mjs +0 -2281
- package/dist/content-DFaIEDj9.mjs.map +0 -1
- package/dist/content-list-query-Dra4Dalc.d.mts +0 -26
- package/dist/content-list-query-Dra4Dalc.d.mts.map +0 -1
- package/dist/content-policy-DFNw91-d.mjs +0 -46
- package/dist/content-policy-DFNw91-d.mjs.map +0 -1
- package/dist/content-repair-Q8IUrpLL.mjs +0 -442
- package/dist/content-repair-Q8IUrpLL.mjs.map +0 -1
- package/dist/context-BeGccdUX.mjs +0 -1921
- package/dist/context-BeGccdUX.mjs.map +0 -1
- package/dist/cron-7DCJwPXq.mjs +0 -284
- package/dist/cron-7DCJwPXq.mjs.map +0 -1
- package/dist/dashboard-DjrB-xxZ.mjs +0 -151
- package/dist/dashboard-DjrB-xxZ.mjs.map +0 -1
- package/dist/default-DQ6vsviu.mjs +0 -81
- package/dist/default-DQ6vsviu.mjs.map +0 -1
- package/dist/deferred-tasks-BPxXUB4x.d.mts +0 -9
- package/dist/deferred-tasks-BPxXUB4x.d.mts.map +0 -1
- package/dist/dev-typegen-CZaPLl3V.mjs +0 -29
- package/dist/dev-typegen-CZaPLl3V.mjs.map +0 -1
- package/dist/device-flow-CRd6DXmW.mjs +0 -466
- package/dist/device-flow-CRd6DXmW.mjs.map +0 -1
- package/dist/digest-BarSh-pN.mjs +0 -194
- package/dist/digest-BarSh-pN.mjs.map +0 -1
- package/dist/direct-executor-BmuFJJZV.mjs +0 -104
- package/dist/direct-executor-BmuFJJZV.mjs.map +0 -1
- package/dist/dist-BNwANFJR.mjs +0 -810
- package/dist/dist-BNwANFJR.mjs.map +0 -1
- package/dist/email-console-bJn3ctqA.mjs +0 -50
- package/dist/email-console-bJn3ctqA.mjs.map +0 -1
- package/dist/email-locale-CN6WAMx0.mjs +0 -25
- package/dist/email-locale-CN6WAMx0.mjs.map +0 -1
- package/dist/emdash-runtime-DAjghyEG.mjs +0 -4186
- package/dist/emdash-runtime-DAjghyEG.mjs.map +0 -1
- package/dist/enrich-Dl3kZ-lP.mjs +0 -194
- package/dist/enrich-Dl3kZ-lP.mjs.map +0 -1
- package/dist/entry-lock-BJYdBfXb.mjs +0 -1097
- package/dist/entry-lock-BJYdBfXb.mjs.map +0 -1
- package/dist/entry-lock-DHQOxN7R.mjs +0 -163
- package/dist/entry-lock-DHQOxN7R.mjs.map +0 -1
- package/dist/entry-lock-DVhCt49A.d.mts +0 -4368
- package/dist/entry-lock-DVhCt49A.d.mts.map +0 -1
- package/dist/entry-locks-D0foLdjQ.mjs +0 -167
- package/dist/entry-locks-D0foLdjQ.mjs.map +0 -1
- package/dist/error-CfvOHP0M.mjs +0 -90
- package/dist/error-CfvOHP0M.mjs.map +0 -1
- package/dist/errors-BF0_LFhW.mjs +0 -460
- package/dist/errors-BF0_LFhW.mjs.map +0 -1
- package/dist/errors-DgE1zJGG.mjs +0 -120
- package/dist/errors-DgE1zJGG.mjs.map +0 -1
- package/dist/escape-CuaZhZvL.mjs +0 -9
- package/dist/escape-CuaZhZvL.mjs.map +0 -1
- package/dist/fence-CMDXJYc_.mjs +0 -162
- package/dist/fence-CMDXJYc_.mjs.map +0 -1
- package/dist/field-defs-cache-HIgBbqZZ.mjs +0 -77
- package/dist/field-defs-cache-HIgBbqZZ.mjs.map +0 -1
- package/dist/focal-point-Gr34o-4o.mjs +0 -23
- package/dist/focal-point-Gr34o-4o.mjs.map +0 -1
- package/dist/fts-manager-S-K0laoS.mjs +0 -443
- package/dist/fts-manager-S-K0laoS.mjs.map +0 -1
- package/dist/gc-DjKY0Ay1.mjs +0 -75
- package/dist/gc-DjKY0Ay1.mjs.map +0 -1
- package/dist/hash-SDAW6zC-.mjs +0 -37
- package/dist/hash-SDAW6zC-.mjs.map +0 -1
- package/dist/hook-result-B5bXDgS2.d.mts +0 -31
- package/dist/hook-result-B5bXDgS2.d.mts.map +0 -1
- package/dist/hreflang-D5uNnsGJ.d.mts +0 -50
- package/dist/hreflang-D5uNnsGJ.d.mts.map +0 -1
- package/dist/hreflang-DMFBL2eb.mjs +0 -105
- package/dist/hreflang-DMFBL2eb.mjs.map +0 -1
- package/dist/http-route-dispatch-BitqzwMy.mjs +0 -339
- package/dist/http-route-dispatch-BitqzwMy.mjs.map +0 -1
- package/dist/identity-IyNWwXr6.mjs +0 -35
- package/dist/identity-IyNWwXr6.mjs.map +0 -1
- package/dist/import-C3JZplWq.mjs +0 -2020
- package/dist/import-C3JZplWq.mjs.map +0 -1
- package/dist/index-D-hdtLe8.d.mts +0 -623
- package/dist/index-D-hdtLe8.d.mts.map +0 -1
- package/dist/install-finalization-DfM1fj5x.mjs +0 -58
- package/dist/install-finalization-DfM1fj5x.mjs.map +0 -1
- package/dist/keys-yxLm5l35.mjs +0 -61
- package/dist/keys-yxLm5l35.mjs.map +0 -1
- package/dist/kinds-CRu_T5T5.mjs +0 -961
- package/dist/kinds-CRu_T5T5.mjs.map +0 -1
- package/dist/lifecycle-BzeGPeQ-.mjs +0 -25
- package/dist/lifecycle-BzeGPeQ-.mjs.map +0 -1
- package/dist/load-XgJiE6_3.mjs +0 -28
- package/dist/load-XgJiE6_3.mjs.map +0 -1
- package/dist/loader-BzqeR1ZN.mjs +0 -1086
- package/dist/loader-BzqeR1ZN.mjs.map +0 -1
- package/dist/loops-BQSzv5Zn.mjs +0 -203
- package/dist/loops-BQSzv5Zn.mjs.map +0 -1
- package/dist/manifest-builder-B8zR-73J.mjs +0 -189
- package/dist/manifest-builder-B8zR-73J.mjs.map +0 -1
- package/dist/manifest-schema-CY3zng_M.mjs +0 -492
- package/dist/manifest-schema-CY3zng_M.mjs.map +0 -1
- package/dist/media-Pu1L6xir.mjs +0 -408
- package/dist/media-Pu1L6xir.mjs.map +0 -1
- package/dist/media-Qp5S3eH3.mjs +0 -355
- package/dist/media-Qp5S3eH3.mjs.map +0 -1
- package/dist/media-allowlist-BSoa9Vvm.mjs +0 -44
- package/dist/media-allowlist-BSoa9Vvm.mjs.map +0 -1
- package/dist/media-folders-BE0oo2eX.mjs +0 -210
- package/dist/media-folders-BE0oo2eX.mjs.map +0 -1
- package/dist/media-h7tOiDLj.mjs +0 -221
- package/dist/media-h7tOiDLj.mjs.map +0 -1
- package/dist/media-url-jArBpzIG.mjs +0 -43
- package/dist/media-url-jArBpzIG.mjs.map +0 -1
- package/dist/media-usage-My-Vi-ex.mjs +0 -1629
- package/dist/media-usage-My-Vi-ex.mjs.map +0 -1
- package/dist/media-usage-activation-HKp7zk7W.mjs +0 -78
- package/dist/media-usage-activation-HKp7zk7W.mjs.map +0 -1
- package/dist/media-usage-work-C2OZLCAd.mjs +0 -586
- package/dist/media-usage-work-C2OZLCAd.mjs.map +0 -1
- package/dist/media-usage-work-DwUNbgvS.mjs +0 -152
- package/dist/media-usage-work-DwUNbgvS.mjs.map +0 -1
- package/dist/menus-BM7BPLOb.mjs +0 -139
- package/dist/menus-BM7BPLOb.mjs.map +0 -1
- package/dist/menus-ITeAbcIF.mjs +0 -753
- package/dist/menus-ITeAbcIF.mjs.map +0 -1
- package/dist/menus-TsSGmSR4.mjs +0 -3608
- package/dist/menus-TsSGmSR4.mjs.map +0 -1
- package/dist/mime-ChLqYwDk.mjs +0 -36
- package/dist/mime-ChLqYwDk.mjs.map +0 -1
- package/dist/mode-CCuKXUci.mjs +0 -23
- package/dist/mode-CCuKXUci.mjs.map +0 -1
- package/dist/node-sqlite-compat-CONMsl-x.mjs +0 -46
- package/dist/node-sqlite-compat-CONMsl-x.mjs.map +0 -1
- package/dist/normalize-CO2P8-iE.mjs +0 -191
- package/dist/normalize-CO2P8-iE.mjs.map +0 -1
- package/dist/oauth-authorization-DlMIL5_h.mjs +0 -274
- package/dist/oauth-authorization-DlMIL5_h.mjs.map +0 -1
- package/dist/oauth-clients-Djq9r_KP.mjs +0 -266
- package/dist/oauth-clients-Djq9r_KP.mjs.map +0 -1
- package/dist/oauth-state-store-BbjnZXNC.mjs +0 -50
- package/dist/oauth-state-store-BbjnZXNC.mjs.map +0 -1
- package/dist/oauth-user-lookup-vue9a64A.mjs +0 -26
- package/dist/oauth-user-lookup-vue9a64A.mjs.map +0 -1
- package/dist/options-SQCHQCPH.d.mts +0 -81
- package/dist/options-SQCHQCPH.d.mts.map +0 -1
- package/dist/parse-CVgO-Qai.d.mts +0 -168
- package/dist/parse-CVgO-Qai.d.mts.map +0 -1
- package/dist/parse-DY6OD1j_.mjs +0 -81
- package/dist/parse-DY6OD1j_.mjs.map +0 -1
- package/dist/passkey-config-X_H8128a.mjs +0 -46
- package/dist/passkey-config-X_H8128a.mjs.map +0 -1
- package/dist/paths-JxokKIbr.mjs +0 -77
- package/dist/paths-JxokKIbr.mjs.map +0 -1
- package/dist/patterns-BhAvqO0S.mjs +0 -145
- package/dist/patterns-BhAvqO0S.mjs.map +0 -1
- package/dist/placeholder-MSEd8gty.d.mts +0 -306
- package/dist/placeholder-MSEd8gty.d.mts.map +0 -1
- package/dist/plan-xjcWrAah.mjs +0 -135
- package/dist/plan-xjcWrAah.mjs.map +0 -1
- package/dist/plugins-DUXUt8B9.mjs +0 -277
- package/dist/plugins-DUXUt8B9.mjs.map +0 -1
- package/dist/policy-CUvwAARc.mjs +0 -45
- package/dist/policy-CUvwAARc.mjs.map +0 -1
- package/dist/portable-text-5O_ZGUjB.mjs +0 -307
- package/dist/portable-text-5O_ZGUjB.mjs.map +0 -1
- package/dist/preview-CIUcPYr6.mjs +0 -107
- package/dist/preview-CIUcPYr6.mjs.map +0 -1
- package/dist/protocol-B97DZaFr.d.mts +0 -61
- package/dist/protocol-B97DZaFr.d.mts.map +0 -1
- package/dist/public-submission-BFBUrqst.mjs +0 -250
- package/dist/public-submission-BFBUrqst.mjs.map +0 -1
- package/dist/public-url-Bn7U0D7-.mjs +0 -101
- package/dist/public-url-Bn7U0D7-.mjs.map +0 -1
- package/dist/query-B48peoRT.mjs +0 -890
- package/dist/query-B48peoRT.mjs.map +0 -1
- package/dist/query-C4IH0uhR.mjs +0 -41
- package/dist/query-C4IH0uhR.mjs.map +0 -1
- package/dist/rate-limit-BZ6nybgc.mjs +0 -120
- package/dist/rate-limit-BZ6nybgc.mjs.map +0 -1
- package/dist/receipt-B4wJj5Jq.mjs +0 -43
- package/dist/receipt-B4wJj5Jq.mjs.map +0 -1
- package/dist/redirect-Cd1E7VUt.mjs +0 -17
- package/dist/redirect-Cd1E7VUt.mjs.map +0 -1
- package/dist/redirect-iJf5fltD.mjs +0 -415
- package/dist/redirect-iJf5fltD.mjs.map +0 -1
- package/dist/redirects-CYu1u1aK.mjs +0 -446
- package/dist/redirects-CYu1u1aK.mjs.map +0 -1
- package/dist/redirects-Cv9apQKj.mjs +0 -148
- package/dist/redirects-Cv9apQKj.mjs.map +0 -1
- package/dist/relation-M4NUCb0b.mjs +0 -334
- package/dist/relation-M4NUCb0b.mjs.map +0 -1
- package/dist/relations-CCif47Oq.mjs +0 -403
- package/dist/relations-CCif47Oq.mjs.map +0 -1
- package/dist/relations-CK9CYUyr.mjs +0 -310
- package/dist/relations-CK9CYUyr.mjs.map +0 -1
- package/dist/request-cache-DzLYKm_x.mjs +0 -104
- package/dist/request-cache-DzLYKm_x.mjs.map +0 -1
- package/dist/request-meta-sZXt3sRo.mjs +0 -141
- package/dist/request-meta-sZXt3sRo.mjs.map +0 -1
- package/dist/resolve-2WbQsCvT.mjs +0 -199
- package/dist/resolve-2WbQsCvT.mjs.map +0 -1
- package/dist/route-wire--1hLuACz.mjs +0 -197
- package/dist/route-wire--1hLuACz.mjs.map +0 -1
- package/dist/routes-CUsM40Cc.mjs +0 -272
- package/dist/routes-CUsM40Cc.mjs.map +0 -1
- package/dist/runner-1y4J9fJB.mjs +0 -5742
- package/dist/runner-1y4J9fJB.mjs.map +0 -1
- package/dist/runner-D5_LzKXI.d.mts +0 -68
- package/dist/runner-D5_LzKXI.d.mts.map +0 -1
- package/dist/schema-BABiRdsH.mjs +0 -13
- package/dist/schema-BABiRdsH.mjs.map +0 -1
- package/dist/schema-Du3MzCG0.mjs +0 -446
- package/dist/schema-Du3MzCG0.mjs.map +0 -1
- package/dist/scopes-Dbk-ElhT.mjs +0 -34
- package/dist/scopes-Dbk-ElhT.mjs.map +0 -1
- package/dist/search-Bt1PL1jf.mjs +0 -448
- package/dist/search-Bt1PL1jf.mjs.map +0 -1
- package/dist/sections-Ddptx_o9.mjs +0 -346
- package/dist/sections-Ddptx_o9.mjs.map +0 -1
- package/dist/seo-CZT2oLtT.mjs +0 -114
- package/dist/seo-CZT2oLtT.mjs.map +0 -1
- package/dist/seo-D5w3-Azy.mjs +0 -133
- package/dist/seo-D5w3-Azy.mjs.map +0 -1
- package/dist/service-DuykXPyW.mjs +0 -150
- package/dist/service-DuykXPyW.mjs.map +0 -1
- package/dist/session-user-BhaVp8w0.mjs +0 -51
- package/dist/session-user-BhaVp8w0.mjs.map +0 -1
- package/dist/settings-2xNYr90o.mjs +0 -51
- package/dist/settings-2xNYr90o.mjs.map +0 -1
- package/dist/settings-CgjRWGW9.mjs +0 -439
- package/dist/settings-CgjRWGW9.mjs.map +0 -1
- package/dist/settings-DYidIB26.mjs +0 -61
- package/dist/settings-DYidIB26.mjs.map +0 -1
- package/dist/setup-complete-bOogcHWG.mjs +0 -26
- package/dist/setup-complete-bOogcHWG.mjs.map +0 -1
- package/dist/setup-nonce-0EGuKM4s.mjs +0 -25
- package/dist/setup-nonce-0EGuKM4s.mjs.map +0 -1
- package/dist/site-settings-DOgRUoFX.mjs +0 -50
- package/dist/site-settings-DOgRUoFX.mjs.map +0 -1
- package/dist/site-url-BU_bbxwj.mjs +0 -16
- package/dist/site-url-BU_bbxwj.mjs.map +0 -1
- package/dist/snapshot-CugwUqKQ.mjs +0 -237
- package/dist/snapshot-CugwUqKQ.mjs.map +0 -1
- package/dist/ssrf-CJUnJymJ.mjs +0 -406
- package/dist/ssrf-CJUnJymJ.mjs.map +0 -1
- package/dist/stage-DgrBz_du.mjs +0 -662
- package/dist/stage-DgrBz_du.mjs.map +0 -1
- package/dist/state-DDiPySSG.mjs +0 -153
- package/dist/state-DDiPySSG.mjs.map +0 -1
- package/dist/status-DZKIG4a9.mjs +0 -30
- package/dist/status-DZKIG4a9.mjs.map +0 -1
- package/dist/storage-query-Bdv7A62k.d.mts +0 -1576
- package/dist/storage-query-Bdv7A62k.d.mts.map +0 -1
- package/dist/tar-_1AgTYFu.mjs +0 -449
- package/dist/tar-_1AgTYFu.mjs.map +0 -1
- package/dist/target-_7pn2Gbg.mjs +0 -27
- package/dist/target-_7pn2Gbg.mjs.map +0 -1
- package/dist/taxonomies-C1IwNe8Q.mjs +0 -697
- package/dist/taxonomies-C1IwNe8Q.mjs.map +0 -1
- package/dist/taxonomies-C_XMoRcX.mjs +0 -939
- package/dist/taxonomies-C_XMoRcX.mjs.map +0 -1
- package/dist/taxonomy-C3Fu1YeH.mjs +0 -525
- package/dist/taxonomy-C3Fu1YeH.mjs.map +0 -1
- package/dist/tokens-u78jETkX.mjs +0 -171
- package/dist/tokens-u78jETkX.mjs.map +0 -1
- package/dist/transaction-BcJO4Tmc.mjs +0 -29
- package/dist/transaction-BcJO4Tmc.mjs.map +0 -1
- package/dist/transfer-BhUCrgDc.mjs +0 -8675
- package/dist/transfer-BhUCrgDc.mjs.map +0 -1
- package/dist/transfer-DIGvR9Af.mjs +0 -166
- package/dist/transfer-DIGvR9Af.mjs.map +0 -1
- package/dist/transfer-download-Pml7RMio.mjs +0 -33
- package/dist/transfer-download-Pml7RMio.mjs.map +0 -1
- package/dist/transformations-9gfpwY65.mjs +0 -196
- package/dist/transformations-9gfpwY65.mjs.map +0 -1
- package/dist/transport-BIfH7_hx.d.mts +0 -42
- package/dist/transport-BIfH7_hx.d.mts.map +0 -1
- package/dist/transport-DKWIq53q.mjs +0 -136
- package/dist/transport-DKWIq53q.mjs.map +0 -1
- package/dist/trusted-proxy-tKiVtyTD.mjs +0 -51
- package/dist/trusted-proxy-tKiVtyTD.mjs.map +0 -1
- package/dist/turnstile-DpsoyOfg.mjs +0 -53
- package/dist/turnstile-DpsoyOfg.mjs.map +0 -1
- package/dist/types-B4sUC2p1.d.mts +0 -198
- package/dist/types-B4sUC2p1.d.mts.map +0 -1
- package/dist/types-B7Lmsh1E.d.mts +0 -887
- package/dist/types-B7Lmsh1E.d.mts.map +0 -1
- package/dist/types-BZakh7Ym.mjs +0 -22
- package/dist/types-BZakh7Ym.mjs.map +0 -1
- package/dist/types-B_CppEmv.d.mts +0 -264
- package/dist/types-B_CppEmv.d.mts.map +0 -1
- package/dist/types-BcrBrE6-.mjs +0 -159
- package/dist/types-BcrBrE6-.mjs.map +0 -1
- package/dist/types-BkUg2yHx.mjs +0 -92
- package/dist/types-BkUg2yHx.mjs.map +0 -1
- package/dist/types-CAZgq9fO.d.mts +0 -124
- package/dist/types-CAZgq9fO.d.mts.map +0 -1
- package/dist/types-Cd-O03VG.d.mts +0 -363
- package/dist/types-Cd-O03VG.d.mts.map +0 -1
- package/dist/types-Cr-Kg-3D.d.mts +0 -1641
- package/dist/types-Cr-Kg-3D.d.mts.map +0 -1
- package/dist/types-D2fvWd1i.d.mts +0 -359
- package/dist/types-D2fvWd1i.d.mts.map +0 -1
- package/dist/types-D7HUsguf.mjs +0 -77
- package/dist/types-D7HUsguf.mjs.map +0 -1
- package/dist/types-DA1Te3Oy.d.mts +0 -198
- package/dist/types-DA1Te3Oy.d.mts.map +0 -1
- package/dist/types-DhV6Q5bP.mjs +0 -18
- package/dist/types-DhV6Q5bP.mjs.map +0 -1
- package/dist/types-DnnVSl6Y.d.mts +0 -85
- package/dist/types-DnnVSl6Y.d.mts.map +0 -1
- package/dist/types-VT4-BugW.mjs +0 -16
- package/dist/types-VT4-BugW.mjs.map +0 -1
- package/dist/upload-attempts-B68ZGNu9.mjs +0 -32
- package/dist/upload-attempts-B68ZGNu9.mjs.map +0 -1
- package/dist/url-pattern-CCeZMGOh.mjs +0 -33
- package/dist/url-pattern-CCeZMGOh.mjs.map +0 -1
- package/dist/user-DDSL-sGw.mjs +0 -171
- package/dist/user-DDSL-sGw.mjs.map +0 -1
- package/dist/utils-DdhmO9w9.mjs +0 -439
- package/dist/utils-DdhmO9w9.mjs.map +0 -1
- package/dist/validate-CHosPEhs.mjs +0 -453
- package/dist/validate-CHosPEhs.mjs.map +0 -1
- package/dist/validate-CM79QXYE.d.mts +0 -432
- package/dist/validate-CM79QXYE.d.mts.map +0 -1
- package/dist/validation-BFecQczG.mjs +0 -218
- package/dist/validation-BFecQczG.mjs.map +0 -1
- package/dist/version-BRZXVifl.mjs +0 -9
- package/dist/version-BRZXVifl.mjs.map +0 -1
- package/dist/version-DUQDHgrf.mjs +0 -7
- package/dist/version-DUQDHgrf.mjs.map +0 -1
- package/dist/widgets-WdV72brA.mjs +0 -126
- package/dist/widgets-WdV72brA.mjs.map +0 -1
- package/dist/wxr-taxonomies-BHTdqkmZ.d.mts +0 -34
- package/dist/wxr-taxonomies-BHTdqkmZ.d.mts.map +0 -1
- package/dist/wxr-taxonomies-DP6ESgMN.mjs +0 -372
- package/dist/wxr-taxonomies-DP6ESgMN.mjs.map +0 -1
- package/dist/zod-generator-D6uE4WiS.mjs +0 -389
- package/dist/zod-generator-D6uE4WiS.mjs.map +0 -1
- package/src/astro/routes/api/relations/[id]/translations.ts +0 -33
- /package/dist/{api-tokens-CE1ECIg0.mjs → api-tokens-FaG1zPsj.mjs} +0 -0
- /package/dist/{ssrf-DpBH7aOt.mjs → ssrf-DZOt8Qof.mjs} +0 -0
|
@@ -1,4186 +0,0 @@
|
|
|
1
|
-
import { r as MIGRATION_RACE_WAIT_MS, t as ConcurrentMigrationTimeoutError } from "./runner-1y4J9fJB.mjs";
|
|
2
|
-
import { n as isMissingTableError } from "./db-errors-CBD5yMro.mjs";
|
|
3
|
-
import { d as isSqlite, m as listTablesLike } from "./dialect-helpers-7_IqPdh9.mjs";
|
|
4
|
-
import { r as getI18nConfig, s as resolveContentCreateLocale } from "./config-CvmjJcVw.mjs";
|
|
5
|
-
import { S as definePlugin, _ as isContentSaveRejection, b as getActiveContentSaveHookName, c as inspectSandboxHookResult, g as ContentSaveRejectedError, h as EmailPipeline, x as resolveExclusiveHooks, y as createHookPipeline } from "./menus-TsSGmSR4.mjs";
|
|
6
|
-
import { t as after } from "./after-PcEvMY_6.mjs";
|
|
7
|
-
import { getRequestContext } from "./request-context.mjs";
|
|
8
|
-
import { t as OptionsRepository } from "./options-DhrI7eMv.mjs";
|
|
9
|
-
import { n as decodeBase64Bytes } from "./base64-DvnNgP7e.mjs";
|
|
10
|
-
import { n as initWithLock, t as createInitLock } from "./init-lock-4eRRB4Le.mjs";
|
|
11
|
-
import { r as singleFlightCached, t as createSingleFlightCache } from "./single-flight-cache-B5O2qhO1.mjs";
|
|
12
|
-
import { o as validateEncryptionKeyAtStartup } from "./secrets-CIi0nFiR.mjs";
|
|
13
|
-
import { i as RevisionRepository, o as keepKnownFields, s as staleStoredKeys, t as ContentRepository } from "./content-DFaIEDj9.mjs";
|
|
14
|
-
import { n as ContentMutationConflictError } from "./types-BkUg2yHx.mjs";
|
|
15
|
-
import { t as MediaRepository } from "./media-Qp5S3eH3.mjs";
|
|
16
|
-
import { t as CommentRepository } from "./comment-CrBl22OF.mjs";
|
|
17
|
-
import { t as AuditRepository } from "./audit-RfCugiSB.mjs";
|
|
18
|
-
import { d as createTaxonomyAccessWithWrite, l as createSiteInfo, r as createCommentAccess, y as updatePluginMediaMetadata } from "./context-BeGccdUX.mjs";
|
|
19
|
-
import { r as requestCached } from "./request-cache-DzLYKm_x.mjs";
|
|
20
|
-
import { n as normalizeMediaValue } from "./normalize-CO2P8-iE.mjs";
|
|
21
|
-
import { n as normalizeMime, t as matchesMimeAllowlist } from "./mime-ChLqYwDk.mjs";
|
|
22
|
-
import { t as enrichImageMetadata } from "./enrich-Dl3kZ-lP.mjs";
|
|
23
|
-
import { i as scheduledPolicyRejectionKey } from "./content-policy-DFNw91-d.mjs";
|
|
24
|
-
import { t as FTSManager } from "./fts-manager-S-K0laoS.mjs";
|
|
25
|
-
import { m as selectTaxonomyDefs, s as getDb } from "./loader-BzqeR1ZN.mjs";
|
|
26
|
-
import { kyselyLogOption } from "./database/instrumentation.mjs";
|
|
27
|
-
import { r as primeRegisteredCollections } from "./collection-slugs-cache-CjGk__zp.mjs";
|
|
28
|
-
import { A as handleContentDiscardDraft, B as handleContentTranslations, D as handleContentCountTrashed, E as handleContentCountScheduled, F as handleContentListTrashed, G as validateRev, H as handleContentUnschedule, I as handleContentPermanentDelete, L as handleContentPublish, M as handleContentGet, N as handleContentGetIncludingTrashed, O as handleContentCreate, P as handleContentList, R as handleContentRestore, S as buildManifestCollections, T as handleContentCompare, U as handleContentUpdate, V as handleContentUnpublish, W as handleScheduledPolicyRejection, b as handleRevisionList, h as syncDeclaredStorageIndexes, j as handleContentDuplicate, k as handleContentDelete, p as loadBundleFromR2, w as handleContentAuthors, x as handleRevisionRestore, y as handleRevisionGet, z as handleContentSchedule } from "./api-cFFZpyS0.mjs";
|
|
29
|
-
import { r as activateMediaUsageCapture } from "./activation-LOkDM5WT.mjs";
|
|
30
|
-
import { A as refreshContentMediaUsageAfterWrite, H as markContentMediaUsageCollectionStale, O as deleteContentMediaUsage, W as MediaUsageRepository, a as SchemaRegistry, i as SchemaError, k as findNonTranslatableSiblingContentIds } from "./block-type-registry-DUI_3KxT.mjs";
|
|
31
|
-
import { n as computeContentHash, r as hashString } from "./hash-SDAW6zC-.mjs";
|
|
32
|
-
import { n as recordSchedulerHeartbeatSafely } from "./dashboard-DjrB-xxZ.mjs";
|
|
33
|
-
import { i as resolveBlockTypes, r as normalizeBlocksData } from "./block-values-C_VIPr5t.mjs";
|
|
34
|
-
import { t as removeUploadAttempt } from "./upload-attempts-B68ZGNu9.mjs";
|
|
35
|
-
import { a as handleMediaReplaceMetadata, i as handleMediaList, n as handleMediaDelete, o as handleMediaUpdate, r as handleMediaGet, t as handleMediaCreate } from "./media-h7tOiDLj.mjs";
|
|
36
|
-
import { s as processMediaUsageWorkAfterWrite } from "./media-usage-My-Vi-ex.mjs";
|
|
37
|
-
import { t as ErrorCode } from "./errors-BF0_LFhW.mjs";
|
|
38
|
-
import { t as PluginStateRepository } from "./state-DDiPySSG.mjs";
|
|
39
|
-
import { s as invalidateSiteSettingsCache } from "./settings-CgjRWGW9.mjs";
|
|
40
|
-
import { i as normalizeManifestRoute$1, r as normalizeManifestHook, t as HOOK_NAMES } from "./manifest-schema-CY3zng_M.mjs";
|
|
41
|
-
import { i as getSandboxRouteErrorDetails } from "./types-D7HUsguf.mjs";
|
|
42
|
-
import { s as normalizePluginCapabilities } from "./types-BZakh7Ym.mjs";
|
|
43
|
-
import { a as ssrfSafeFetch, t as SsrfError } from "./ssrf-CJUnJymJ.mjs";
|
|
44
|
-
import { c as resolveRegistryConfigForSandbox, r as getRegistryConfigInput, s as resolveManifestRegistryConfig } from "./config-IhybvkIQ.mjs";
|
|
45
|
-
import { n as DEFAULT_MAX_UPLOAD_SIZE, r as formatFileSize, t as CONTENT_TYPE_RE } from "./media-Pu1L6xir.mjs";
|
|
46
|
-
import { t as getTrustedProxyHeaders } from "./trusted-proxy-tKiVtyTD.mjs";
|
|
47
|
-
import { n as sanitizeHeadersForSandbox, t as extractRequestMeta } from "./request-meta-sZXt3sRo.mjs";
|
|
48
|
-
import { adaptSandboxEntry } from "./plugins/adapt-sandbox-entry.mjs";
|
|
49
|
-
import { t as GLOBAL_UPLOAD_ALLOWLIST } from "./media-allowlist-BSoa9Vvm.mjs";
|
|
50
|
-
import { n as CronExecutor } from "./cron-7DCJwPXq.mjs";
|
|
51
|
-
import { r as devConsoleEmailDeliver, t as DEV_CONSOLE_EMAIL_PLUGIN_ID } from "./email-console-bJn3ctqA.mjs";
|
|
52
|
-
import { n as assertSiteWriteAllowed } from "./fence-CMDXJYc_.mjs";
|
|
53
|
-
import { t as PluginRouteRequestError } from "./route-wire--1hLuACz.mjs";
|
|
54
|
-
import { a as toRouteCallerInfo, n as buildRouteMeta, r as parseRouteInput, t as PluginRouteRegistry } from "./routes-CUsM40Cc.mjs";
|
|
55
|
-
import { n as enforceRuntimeMigrationPolicy, t as PendingMigrationsError } from "./policy-CUvwAARc.mjs";
|
|
56
|
-
import { n as VERSION, t as COMMIT } from "./version-DUQDHgrf.mjs";
|
|
57
|
-
import { t as getSiteBaseUrl } from "./site-url-BU_bbxwj.mjs";
|
|
58
|
-
import { t as getAuthMode } from "./mode-CCuKXUci.mjs";
|
|
59
|
-
import { n as lookupContentAuthor, r as sendCommentNotification, t as submitPublicComment } from "./public-submission-BFBUrqst.mjs";
|
|
60
|
-
import { u as maybeRunScheduledBackup } from "./backup-CBu0LrXf.mjs";
|
|
61
|
-
import { t as cleanupExpiredChallenges } from "./challenge-store-ppU6Uv13.mjs";
|
|
62
|
-
import { n as createComment, r as moderateComment, t as CommentStatusConflictError } from "./service-DuykXPyW.mjs";
|
|
63
|
-
import { isRecord } from "./plugin-utils.mjs";
|
|
64
|
-
import { n as enableRuntimePlugin, t as disableRuntimePlugin } from "./lifecycle-BzeGPeQ-.mjs";
|
|
65
|
-
import { Kysely, sql } from "kysely";
|
|
66
|
-
import { z } from "zod";
|
|
67
|
-
import { ulid } from "ulidx";
|
|
68
|
-
import { Permissions } from "@emdash-cms/auth";
|
|
69
|
-
import { isJsonPostRouteContract } from "@emdash-cms/plugin-types";
|
|
70
|
-
import * as path from "node:path";
|
|
71
|
-
import virtualConfig from "virtual:emdash/config";
|
|
72
|
-
import { getLocaleDir, resolveLocale } from "@emdash-cms/admin/locales";
|
|
73
|
-
import { normalizePluginPagePath, validateBlockResponse, validateContentEditorActionResponse } from "@emdash-cms/blocks/server";
|
|
74
|
-
import { createKyselyAdapter } from "@emdash-cms/auth/adapters/kysely";
|
|
75
|
-
|
|
76
|
-
//#region src/api/handlers/media-upload.ts
|
|
77
|
-
/**
|
|
78
|
-
* Programmatic media upload handler (MCP `media_upload` tool).
|
|
79
|
-
*
|
|
80
|
-
* Accepts file bytes as base64 or fetches them from an external URL
|
|
81
|
-
* (SSRF-guarded), then runs the same pipeline as the multipart REST
|
|
82
|
-
* upload route: allowlist + size validation, content-hash deduplication,
|
|
83
|
-
* storage upload, image metadata enrichment, and record creation.
|
|
84
|
-
*/
|
|
85
|
-
function fail(code, message) {
|
|
86
|
-
return {
|
|
87
|
-
success: false,
|
|
88
|
-
error: {
|
|
89
|
-
code,
|
|
90
|
-
message
|
|
91
|
-
}
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
/** Same relative-URL shape the REST media routes return. */
|
|
95
|
-
function withUrl(item) {
|
|
96
|
-
return {
|
|
97
|
-
...item,
|
|
98
|
-
url: `/_emdash/api/media/file/${item.storageKey}`
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
/** Strip parameters from a Content-Type header value (e.g. '; charset=...'). */
|
|
102
|
-
function bareMime(headerValue) {
|
|
103
|
-
return (headerValue.split(";")[0] ?? "").trim();
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Acquire the file bytes and MIME type from either the base64 payload or
|
|
107
|
-
* the external URL. Returns an error result on any validation failure.
|
|
108
|
-
*/
|
|
109
|
-
async function acquireBytes(input, maxUploadSize) {
|
|
110
|
-
if (input.base64) {
|
|
111
|
-
if (!input.contentType) return fail("VALIDATION_ERROR", "contentType is required when uploading base64 data");
|
|
112
|
-
if (input.base64.length * 3 / 4 > maxUploadSize) return fail("PAYLOAD_TOO_LARGE", `File exceeds maximum size of ${formatFileSize(maxUploadSize)}`);
|
|
113
|
-
try {
|
|
114
|
-
return {
|
|
115
|
-
bytes: decodeBase64Bytes(input.base64),
|
|
116
|
-
mimeType: input.contentType
|
|
117
|
-
};
|
|
118
|
-
} catch {
|
|
119
|
-
return fail("VALIDATION_ERROR", "Invalid base64 data");
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
const url = input.url;
|
|
123
|
-
if (!url) return fail("VALIDATION_ERROR", "Provide exactly one of 'base64' or 'url'");
|
|
124
|
-
let response;
|
|
125
|
-
try {
|
|
126
|
-
response = await ssrfSafeFetch(url, { headers: { accept: "*/*" } });
|
|
127
|
-
} catch (error) {
|
|
128
|
-
if (error instanceof SsrfError) return fail("VALIDATION_ERROR", `URL not allowed: ${error.message}`);
|
|
129
|
-
return fail("FETCH_ERROR", "Failed to fetch file from URL");
|
|
130
|
-
}
|
|
131
|
-
if (!response.ok) return fail("FETCH_ERROR", `Failed to fetch file from URL (HTTP ${response.status})`);
|
|
132
|
-
const contentLength = response.headers.get("Content-Length");
|
|
133
|
-
if (contentLength && parseInt(contentLength, 10) > maxUploadSize) return fail("PAYLOAD_TOO_LARGE", `File exceeds maximum size of ${formatFileSize(maxUploadSize)}`);
|
|
134
|
-
const mimeType = input.contentType ?? bareMime(response.headers.get("Content-Type") ?? "");
|
|
135
|
-
if (!mimeType) return fail("VALIDATION_ERROR", "Could not determine MIME type — pass contentType explicitly");
|
|
136
|
-
return {
|
|
137
|
-
bytes: new Uint8Array(await response.arrayBuffer()),
|
|
138
|
-
mimeType
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Upload a media file from base64 data or an external URL.
|
|
143
|
-
*
|
|
144
|
-
* Mirrors the REST `POST /_emdash/api/media` route: global MIME allowlist,
|
|
145
|
-
* size limit, content-hash dedupe (returns the existing item with
|
|
146
|
-
* `deduplicated: true`), storage upload with cleanup on failure, and
|
|
147
|
-
* image metadata enrichment (dimensions, blurhash, dominant color).
|
|
148
|
-
*/
|
|
149
|
-
async function handleMediaUpload(db, storage, input, hooks = {}) {
|
|
150
|
-
if (!input.base64 === !input.url) return fail("VALIDATION_ERROR", "Provide exactly one of 'base64' or 'url'");
|
|
151
|
-
const rawMax = input.maxUploadSize ?? DEFAULT_MAX_UPLOAD_SIZE;
|
|
152
|
-
if (!Number.isFinite(rawMax) || rawMax <= 0) return fail("CONFIGURATION_ERROR", "Invalid maxUploadSize configuration");
|
|
153
|
-
const acquired = await acquireBytes(input, rawMax);
|
|
154
|
-
if ("success" in acquired) return acquired;
|
|
155
|
-
const { bytes } = acquired;
|
|
156
|
-
if (!CONTENT_TYPE_RE.test(acquired.mimeType)) return fail("VALIDATION_ERROR", "Invalid content type");
|
|
157
|
-
let filename = input.filename;
|
|
158
|
-
let mimeType = normalizeMime(acquired.mimeType);
|
|
159
|
-
let size = bytes.byteLength;
|
|
160
|
-
if (!matchesMimeAllowlist(mimeType, GLOBAL_UPLOAD_ALLOWLIST)) return fail("INVALID_TYPE", "File type not allowed");
|
|
161
|
-
if (bytes.byteLength > rawMax) return fail("PAYLOAD_TOO_LARGE", `File exceeds maximum size of ${formatFileSize(rawMax)}`);
|
|
162
|
-
try {
|
|
163
|
-
if (hooks.beforeUpload) {
|
|
164
|
-
const processed = await hooks.beforeUpload({
|
|
165
|
-
name: filename,
|
|
166
|
-
type: mimeType,
|
|
167
|
-
size: bytes.byteLength
|
|
168
|
-
});
|
|
169
|
-
filename = processed.name;
|
|
170
|
-
mimeType = normalizeMime(processed.type);
|
|
171
|
-
size = processed.size;
|
|
172
|
-
if (!CONTENT_TYPE_RE.test(processed.type)) return fail("VALIDATION_ERROR", "Invalid content type");
|
|
173
|
-
if (!matchesMimeAllowlist(mimeType, GLOBAL_UPLOAD_ALLOWLIST)) return fail("INVALID_TYPE", "File type not allowed");
|
|
174
|
-
}
|
|
175
|
-
const contentHash = await computeContentHash(bytes);
|
|
176
|
-
const repo = new MediaRepository(db);
|
|
177
|
-
const existing = await repo.findByContentHash(contentHash);
|
|
178
|
-
if (existing) return {
|
|
179
|
-
success: true,
|
|
180
|
-
data: {
|
|
181
|
-
item: withUrl(existing),
|
|
182
|
-
deduplicated: true
|
|
183
|
-
}
|
|
184
|
-
};
|
|
185
|
-
const storageKey = `${ulid()}${path.extname(filename)}`;
|
|
186
|
-
await storage.upload({
|
|
187
|
-
key: storageKey,
|
|
188
|
-
body: bytes,
|
|
189
|
-
contentType: mimeType
|
|
190
|
-
});
|
|
191
|
-
try {
|
|
192
|
-
const enriched = await enrichImageMetadata(bytes, mimeType);
|
|
193
|
-
return {
|
|
194
|
-
success: true,
|
|
195
|
-
data: { item: withUrl(await repo.create({
|
|
196
|
-
filename,
|
|
197
|
-
mimeType,
|
|
198
|
-
size,
|
|
199
|
-
width: enriched.width,
|
|
200
|
-
height: enriched.height,
|
|
201
|
-
alt: input.alt,
|
|
202
|
-
storageKey,
|
|
203
|
-
contentHash,
|
|
204
|
-
blurhash: enriched.blurhash,
|
|
205
|
-
dominantColor: enriched.dominantColor,
|
|
206
|
-
authorId: input.authorId
|
|
207
|
-
})) }
|
|
208
|
-
};
|
|
209
|
-
} catch (error) {
|
|
210
|
-
try {
|
|
211
|
-
await storage.delete(storageKey);
|
|
212
|
-
} catch {}
|
|
213
|
-
throw error;
|
|
214
|
-
}
|
|
215
|
-
} catch {
|
|
216
|
-
return fail("UPLOAD_ERROR", "Upload failed");
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
//#endregion
|
|
221
|
-
//#region src/i18n/repair-locale-casing.ts
|
|
222
|
-
/** Rewrite stored locales to the exact casing used by the site configuration. */
|
|
223
|
-
async function repairLocaleCasing(db, configuredLocales) {
|
|
224
|
-
const tableNames = await listTablesLike(db, "ec_%");
|
|
225
|
-
for (const tableName of tableNames) {
|
|
226
|
-
const table = sql.ref(tableName);
|
|
227
|
-
for (const locale of configuredLocales) await sql`
|
|
228
|
-
UPDATE ${table} AS target
|
|
229
|
-
SET locale = ${locale}
|
|
230
|
-
WHERE lower(target.locale) = lower(${locale})
|
|
231
|
-
AND target.locale != ${locale}
|
|
232
|
-
AND NOT EXISTS (
|
|
233
|
-
SELECT 1
|
|
234
|
-
FROM ${table} AS existing
|
|
235
|
-
WHERE existing.slug = target.slug AND existing.locale = ${locale}
|
|
236
|
-
)
|
|
237
|
-
AND target.id = (
|
|
238
|
-
SELECT MIN(candidate.id)
|
|
239
|
-
FROM ${table} AS candidate
|
|
240
|
-
WHERE candidate.slug = target.slug
|
|
241
|
-
AND lower(candidate.locale) = lower(${locale})
|
|
242
|
-
AND candidate.locale != ${locale}
|
|
243
|
-
)
|
|
244
|
-
`.execute(db);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
//#endregion
|
|
249
|
-
//#region src/i18n/taxonomy-locale-diagnostic.ts
|
|
250
|
-
const REPAIR_GUIDE = "https://docs.emdashcms.com/guides/internationalization/#repairing-taxonomy-locale-mismatches";
|
|
251
|
-
async function warnAboutUnconfiguredTaxonomyLocales(db, configuredLocales, definitionLocales) {
|
|
252
|
-
const supportedLocales = configuredLocales.length > 0 ? configuredLocales : ["en"];
|
|
253
|
-
const definitionRows = definitionLocales === void 0 ? await db.selectFrom("_emdash_taxonomy_defs").select("locale").distinct().where("locale", "not in", supportedLocales).execute() : [...new Set(definitionLocales)].filter((locale) => !supportedLocales.includes(locale)).map((locale) => ({ locale }));
|
|
254
|
-
const termRows = await db.selectFrom("taxonomies").select("locale").distinct().where("locale", "not in", supportedLocales).execute();
|
|
255
|
-
const mismatches = [...definitionRows.map(({ locale }) => ({
|
|
256
|
-
source: "definitions",
|
|
257
|
-
locale
|
|
258
|
-
})), ...termRows.map(({ locale }) => ({
|
|
259
|
-
source: "terms",
|
|
260
|
-
locale
|
|
261
|
-
}))].toSorted((a, b) => a.source.localeCompare(b.source) || a.locale.localeCompare(b.locale));
|
|
262
|
-
if (mismatches.length === 0) return;
|
|
263
|
-
const details = mismatches.map(({ source, locale }) => `${source}: ${locale}`).join("; ");
|
|
264
|
-
console.warn(`EmDash: Taxonomy rows use locales outside the configured locales (${supportedLocales.join(", ")}): ${details}. Locale-scoped reads may not return these rows. Review and repair them explicitly: ${REPAIR_GUIDE}`);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
//#endregion
|
|
268
|
-
//#region src/plugins/sandbox/proxy.ts
|
|
269
|
-
const SANDBOX_REJECTION = Symbol("emdash:sandbox-save-rejection");
|
|
270
|
-
const VALID_METADATA_KINDS = new Set([
|
|
271
|
-
"meta",
|
|
272
|
-
"property",
|
|
273
|
-
"link",
|
|
274
|
-
"jsonld"
|
|
275
|
-
]);
|
|
276
|
-
const VALID_LINK_REL = new Set([
|
|
277
|
-
"canonical",
|
|
278
|
-
"alternate",
|
|
279
|
-
"author",
|
|
280
|
-
"license",
|
|
281
|
-
"nlweb",
|
|
282
|
-
"site.standard.document"
|
|
283
|
-
]);
|
|
284
|
-
function isValidMetadataContribution(value) {
|
|
285
|
-
if (!value || typeof value !== "object" || !("kind" in value)) return false;
|
|
286
|
-
const contribution = value;
|
|
287
|
-
if (typeof contribution.kind !== "string" || !VALID_METADATA_KINDS.has(contribution.kind)) return false;
|
|
288
|
-
if (contribution.kind === "meta") return typeof contribution.name === "string" && typeof contribution.content === "string";
|
|
289
|
-
if (contribution.kind === "property") return typeof contribution.property === "string" && typeof contribution.content === "string";
|
|
290
|
-
if (contribution.kind === "link") return typeof contribution.href === "string" && typeof contribution.rel === "string" && VALID_LINK_REL.has(contribution.rel);
|
|
291
|
-
return contribution.graph != null && typeof contribution.graph === "object";
|
|
292
|
-
}
|
|
293
|
-
function getSandboxSaveRejectionDetails(error) {
|
|
294
|
-
if (!(error instanceof ContentSaveRejectedError) || !(SANDBOX_REJECTION in error)) return null;
|
|
295
|
-
const details = Object.getOwnPropertyDescriptor(error, SANDBOX_REJECTION)?.value;
|
|
296
|
-
if (!details || typeof details !== "object") return null;
|
|
297
|
-
if (!("pluginId" in details) || typeof details.pluginId !== "string") return null;
|
|
298
|
-
if (!("reason" in details) || typeof details.reason !== "string") return null;
|
|
299
|
-
return {
|
|
300
|
-
pluginId: details.pluginId,
|
|
301
|
-
reason: details.reason
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
/**
|
|
305
|
-
* Represent an isolate-backed plugin inside the normal host hook pipeline.
|
|
306
|
-
* The proxy contains metadata and RPC handlers only; plugin code remains in
|
|
307
|
-
* the runner isolate.
|
|
308
|
-
*/
|
|
309
|
-
function createSandboxedPluginProxy(manifest, instance) {
|
|
310
|
-
const hooks = {};
|
|
311
|
-
for (const entry of manifest.hooks) {
|
|
312
|
-
const hook = normalizeManifestHook(entry);
|
|
313
|
-
if (hook.name === "page:fragments") continue;
|
|
314
|
-
hooks[hook.name] = {
|
|
315
|
-
handler: async (event) => {
|
|
316
|
-
const result = await instance.invokeHook(hook.name, event);
|
|
317
|
-
if (hook.name === "page:metadata") {
|
|
318
|
-
if (result == null) return null;
|
|
319
|
-
const contributions = (Array.isArray(result) ? result : [result]).filter(isValidMetadataContribution);
|
|
320
|
-
return contributions.length > 0 ? contributions : null;
|
|
321
|
-
}
|
|
322
|
-
if (hook.name !== "content:beforeSave") return result;
|
|
323
|
-
const inspection = inspectSandboxHookResult(result);
|
|
324
|
-
if (inspection.kind === "error") {
|
|
325
|
-
const error = new ContentSaveRejectedError("Save rejected by a sandboxed plugin");
|
|
326
|
-
return Promise.reject(Object.assign(error, { [SANDBOX_REJECTION]: {
|
|
327
|
-
pluginId: manifest.id,
|
|
328
|
-
reason: inspection.error.reason
|
|
329
|
-
} }));
|
|
330
|
-
}
|
|
331
|
-
if (inspection.kind === "malformed") throw new Error(`Sandboxed plugin "${manifest.id}" returned an invalid hook result`);
|
|
332
|
-
return result;
|
|
333
|
-
},
|
|
334
|
-
priority: hook.priority,
|
|
335
|
-
timeout: hook.timeout,
|
|
336
|
-
dependencies: hook.dependencies,
|
|
337
|
-
errorPolicy: hook.errorPolicy,
|
|
338
|
-
exclusive: hook.exclusive
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
return adaptSandboxEntry({ hooks }, {
|
|
342
|
-
id: manifest.id,
|
|
343
|
-
version: manifest.version,
|
|
344
|
-
entrypoint: "",
|
|
345
|
-
format: "standard",
|
|
346
|
-
capabilities: manifest.capabilities,
|
|
347
|
-
allowedHosts: manifest.allowedHosts,
|
|
348
|
-
storage: manifest.storage,
|
|
349
|
-
adminPages: manifest.admin.pages,
|
|
350
|
-
adminWidgets: manifest.admin.widgets,
|
|
351
|
-
editorPanels: manifest.admin.editorPanels,
|
|
352
|
-
editorActions: manifest.admin.editorActions,
|
|
353
|
-
settingsSchema: manifest.admin.settingsSchema,
|
|
354
|
-
portableTextBlocks: manifest.admin.portableTextBlocks,
|
|
355
|
-
fieldWidgets: manifest.admin.fieldWidgets
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
//#endregion
|
|
360
|
-
//#region src/plugins/sandbox/runner-options.ts
|
|
361
|
-
/**
|
|
362
|
-
* Build platform sandbox options with the same normalized site context used
|
|
363
|
-
* by trusted plugin hooks and routes.
|
|
364
|
-
*/
|
|
365
|
-
function createSandboxRunnerOptions(options, siteInfo) {
|
|
366
|
-
return {
|
|
367
|
-
...options,
|
|
368
|
-
siteInfo: createSiteInfo(siteInfo ?? {})
|
|
369
|
-
};
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
//#endregion
|
|
373
|
-
//#region src/media/usage/cleanup.ts
|
|
374
|
-
const MEDIA_USAGE_CLEANUP_CANDIDATE_LIMIT = 250;
|
|
375
|
-
const MEDIA_USAGE_CLEANUP_DELETE_LIMIT = 50;
|
|
376
|
-
const MEDIA_USAGE_CLEANUP_WRITE_LEASE_DELETE_LIMIT = 49;
|
|
377
|
-
const MEDIA_USAGE_CLEANUP_INTERVAL_MS = 60 * 1e3;
|
|
378
|
-
const MEDIA_USAGE_CLEANUP_LEASE_MS = 300 * 1e3;
|
|
379
|
-
const MEDIA_USAGE_CLEANUP_SAFETY_WINDOW_MS = 3600 * 1e3;
|
|
380
|
-
const MEDIA_USAGE_CLEANUP_TIME_BUDGET_MS = 5 * 1e3;
|
|
381
|
-
/**
|
|
382
|
-
* Reclaims a bounded window of obsolete media-usage occurrences.
|
|
383
|
-
*
|
|
384
|
-
* The persisted claim makes a cron tick single-flight across Worker isolates
|
|
385
|
-
* and Node processes.
|
|
386
|
-
*/
|
|
387
|
-
async function cleanupMediaUsage(db) {
|
|
388
|
-
const startedMs = Date.now();
|
|
389
|
-
const canIssueStatement = () => withinBudget(startedMs);
|
|
390
|
-
const repo = new MediaUsageRepository(db);
|
|
391
|
-
const leaseToken = ulid();
|
|
392
|
-
const claim = await repo.claimMediaUsageCleanup({
|
|
393
|
-
leaseToken,
|
|
394
|
-
leaseDurationSeconds: MEDIA_USAGE_CLEANUP_LEASE_MS / 1e3,
|
|
395
|
-
nextEligibleDelaySeconds: MEDIA_USAGE_CLEANUP_INTERVAL_MS / 1e3,
|
|
396
|
-
sweepSafetyWindowSeconds: MEDIA_USAGE_CLEANUP_SAFETY_WINDOW_MS / 1e3
|
|
397
|
-
});
|
|
398
|
-
if (!claim) return emptyResult("skipped", elapsedSince(startedMs));
|
|
399
|
-
let candidateRows = 0;
|
|
400
|
-
let deletedOrphans = 0;
|
|
401
|
-
let deletedStale = 0;
|
|
402
|
-
let deletedAbandoned = 0;
|
|
403
|
-
let deletedWriteLeases = 0;
|
|
404
|
-
let backlogLowerBound = 0;
|
|
405
|
-
let scanHasMore = false;
|
|
406
|
-
let nextCursor = claim.cursor;
|
|
407
|
-
let sweepComplete = false;
|
|
408
|
-
try {
|
|
409
|
-
if (canIssueStatement()) deletedWriteLeases = await repo.deleteExpiredGenerationWriteLeases(MEDIA_USAGE_CLEANUP_WRITE_LEASE_DELETE_LIMIT, cleanupLease(leaseToken), canIssueStatement);
|
|
410
|
-
const cutoff = claim.scanBeforeAt;
|
|
411
|
-
const candidates = await repo.findMediaUsageCleanupCandidates({
|
|
412
|
-
cutoff,
|
|
413
|
-
cursor: claim.cursor,
|
|
414
|
-
limit: MEDIA_USAGE_CLEANUP_CANDIDATE_LIMIT,
|
|
415
|
-
cleanupLease: cleanupLease(leaseToken),
|
|
416
|
-
canIssueStatement
|
|
417
|
-
});
|
|
418
|
-
if (candidates) {
|
|
419
|
-
candidateRows = candidates.length;
|
|
420
|
-
scanHasMore = candidates.length === MEDIA_USAGE_CLEANUP_CANDIDATE_LIMIT;
|
|
421
|
-
const selected = selectCleanupCandidates(candidates, claim.claimedAt);
|
|
422
|
-
backlogLowerBound = selected.orphanIds.length + selected.staleIds.length + selected.abandonedIds.length;
|
|
423
|
-
const completedTargets = /* @__PURE__ */ new Set();
|
|
424
|
-
let canContinue = true;
|
|
425
|
-
if (canIssueStatement() && selected.orphanIds.length > 0) {
|
|
426
|
-
deletedOrphans = await repo.deleteOrphanOccurrencesOlderThan(cutoff, selected.orphanIds.length, {
|
|
427
|
-
candidateIds: selected.orphanIds,
|
|
428
|
-
cleanupLease: cleanupLease(leaseToken),
|
|
429
|
-
canIssueStatement
|
|
430
|
-
});
|
|
431
|
-
canContinue = deletedOrphans === selected.orphanIds.length;
|
|
432
|
-
if (canContinue) completedTargets.add("orphan");
|
|
433
|
-
}
|
|
434
|
-
if (canContinue && canIssueStatement() && selected.staleIds.length > 0) {
|
|
435
|
-
deletedStale = await repo.deleteStaleGenerationsOlderThan(cutoff, selected.staleIds.length, {
|
|
436
|
-
candidateIds: selected.staleIds,
|
|
437
|
-
cleanupLease: cleanupLease(leaseToken),
|
|
438
|
-
canIssueStatement
|
|
439
|
-
});
|
|
440
|
-
canContinue = deletedStale === selected.staleIds.length;
|
|
441
|
-
if (canContinue) completedTargets.add("stale");
|
|
442
|
-
}
|
|
443
|
-
if (canContinue && canIssueStatement() && selected.abandonedIds.length > 0) {
|
|
444
|
-
deletedAbandoned = await repo.deleteAbandonedGenerationsOlderThan(cutoff, selected.abandonedIds.length, {
|
|
445
|
-
candidateIds: selected.abandonedIds,
|
|
446
|
-
cleanupLease: cleanupLease(leaseToken),
|
|
447
|
-
canIssueStatement
|
|
448
|
-
});
|
|
449
|
-
if (deletedAbandoned === selected.abandonedIds.length) completedTargets.add("abandoned");
|
|
450
|
-
}
|
|
451
|
-
const hasIncompleteTargets = selected.entries.some((entry) => entry.target !== null && !completedTargets.has(entry.target));
|
|
452
|
-
sweepComplete = !scanHasMore && selected.entries.length === candidates.length && !hasIncompleteTargets;
|
|
453
|
-
nextCursor = sweepComplete ? null : cursorAfterCompletedCandidates(selected, completedTargets, claim.cursor);
|
|
454
|
-
}
|
|
455
|
-
const durationMs = elapsedSince(startedMs);
|
|
456
|
-
return {
|
|
457
|
-
status: await repo.completeMediaUsageCleanup({
|
|
458
|
-
leaseToken,
|
|
459
|
-
nextCursor,
|
|
460
|
-
sweepComplete,
|
|
461
|
-
candidateCount: candidateRows,
|
|
462
|
-
deletedOrphans,
|
|
463
|
-
deletedStale,
|
|
464
|
-
deletedAbandoned,
|
|
465
|
-
deletedWriteLeases,
|
|
466
|
-
backlogLowerBound,
|
|
467
|
-
scanHasMore,
|
|
468
|
-
durationMs
|
|
469
|
-
}) ? "completed" : "skipped",
|
|
470
|
-
candidateRows,
|
|
471
|
-
deletedRows: deletedOrphans + deletedStale + deletedAbandoned,
|
|
472
|
-
deletedOrphans,
|
|
473
|
-
deletedStale,
|
|
474
|
-
deletedAbandoned,
|
|
475
|
-
deletedWriteLeases,
|
|
476
|
-
backlogLowerBound,
|
|
477
|
-
scanHasMore,
|
|
478
|
-
durationMs
|
|
479
|
-
};
|
|
480
|
-
} catch (error) {
|
|
481
|
-
const durationMs = elapsedSince(startedMs);
|
|
482
|
-
const failures = Math.min(claim.consecutiveFailures + 1, 5);
|
|
483
|
-
try {
|
|
484
|
-
await repo.failMediaUsageCleanup({
|
|
485
|
-
leaseToken,
|
|
486
|
-
retryDelaySeconds: failureDelayMs(failures) / 1e3,
|
|
487
|
-
consecutiveFailures: failures,
|
|
488
|
-
durationMs,
|
|
489
|
-
errorCode: "MEDIA_USAGE_CLEANUP_FAILED"
|
|
490
|
-
});
|
|
491
|
-
} catch (failureError) {
|
|
492
|
-
console.error("[media-usage-cleanup] Failed to record cleanup failure:", failureError);
|
|
493
|
-
}
|
|
494
|
-
console.error("[media-usage-cleanup] Cleanup failed:", error);
|
|
495
|
-
return {
|
|
496
|
-
...emptyResult("failed", durationMs),
|
|
497
|
-
candidateRows,
|
|
498
|
-
deletedRows: deletedOrphans + deletedStale + deletedAbandoned,
|
|
499
|
-
deletedOrphans,
|
|
500
|
-
deletedStale,
|
|
501
|
-
deletedAbandoned,
|
|
502
|
-
deletedWriteLeases,
|
|
503
|
-
backlogLowerBound,
|
|
504
|
-
scanHasMore
|
|
505
|
-
};
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
function selectCleanupCandidates(candidates, activeLeaseAt) {
|
|
509
|
-
const orphanIds = [];
|
|
510
|
-
const staleIds = [];
|
|
511
|
-
const abandonedIds = [];
|
|
512
|
-
const entries = [];
|
|
513
|
-
for (const candidate of candidates) {
|
|
514
|
-
if (hasActiveWriteLease(candidate, activeLeaseAt)) {
|
|
515
|
-
entries.push({
|
|
516
|
-
candidate,
|
|
517
|
-
target: null
|
|
518
|
-
});
|
|
519
|
-
continue;
|
|
520
|
-
}
|
|
521
|
-
const target = cleanupTarget(candidate);
|
|
522
|
-
if (target === null) {
|
|
523
|
-
entries.push({
|
|
524
|
-
candidate,
|
|
525
|
-
target: null
|
|
526
|
-
});
|
|
527
|
-
continue;
|
|
528
|
-
}
|
|
529
|
-
if (orphanIds.length + staleIds.length + abandonedIds.length >= MEDIA_USAGE_CLEANUP_DELETE_LIMIT) break;
|
|
530
|
-
if (target === "orphan") orphanIds.push(candidate.id);
|
|
531
|
-
if (target === "stale") staleIds.push(candidate.id);
|
|
532
|
-
if (target === "abandoned") abandonedIds.push(candidate.id);
|
|
533
|
-
entries.push({
|
|
534
|
-
candidate,
|
|
535
|
-
target
|
|
536
|
-
});
|
|
537
|
-
}
|
|
538
|
-
return {
|
|
539
|
-
orphanIds,
|
|
540
|
-
staleIds,
|
|
541
|
-
abandonedIds,
|
|
542
|
-
entries
|
|
543
|
-
};
|
|
544
|
-
}
|
|
545
|
-
function cleanupTarget(candidate) {
|
|
546
|
-
if (candidate.currentGeneration === null) return "orphan";
|
|
547
|
-
if (candidate.currentGeneration === candidate.generation || candidate.indexedAt === null) return null;
|
|
548
|
-
return candidate.createdAt < candidate.indexedAt ? "stale" : "abandoned";
|
|
549
|
-
}
|
|
550
|
-
function hasActiveWriteLease(candidate, activeLeaseAt) {
|
|
551
|
-
return candidate.writeLeaseExpiresAt !== null && candidate.writeLeaseExpiresAt > activeLeaseAt;
|
|
552
|
-
}
|
|
553
|
-
function cursorFor(candidate) {
|
|
554
|
-
return {
|
|
555
|
-
createdAt: candidate.createdAt,
|
|
556
|
-
id: candidate.id
|
|
557
|
-
};
|
|
558
|
-
}
|
|
559
|
-
function cursorAfterCompletedCandidates(selected, completedTargets, priorCursor) {
|
|
560
|
-
let cursor = priorCursor;
|
|
561
|
-
for (const entry of selected.entries) {
|
|
562
|
-
if (entry.target !== null && !completedTargets.has(entry.target)) break;
|
|
563
|
-
cursor = cursorFor(entry.candidate);
|
|
564
|
-
}
|
|
565
|
-
return cursor;
|
|
566
|
-
}
|
|
567
|
-
function emptyResult(status, durationMs) {
|
|
568
|
-
return {
|
|
569
|
-
status,
|
|
570
|
-
candidateRows: 0,
|
|
571
|
-
deletedRows: 0,
|
|
572
|
-
deletedOrphans: 0,
|
|
573
|
-
deletedStale: 0,
|
|
574
|
-
deletedAbandoned: 0,
|
|
575
|
-
deletedWriteLeases: 0,
|
|
576
|
-
backlogLowerBound: 0,
|
|
577
|
-
scanHasMore: false,
|
|
578
|
-
durationMs
|
|
579
|
-
};
|
|
580
|
-
}
|
|
581
|
-
function withinBudget(startedMs) {
|
|
582
|
-
return elapsedSince(startedMs) < MEDIA_USAGE_CLEANUP_TIME_BUDGET_MS;
|
|
583
|
-
}
|
|
584
|
-
function elapsedSince(startedMs) {
|
|
585
|
-
return Math.max(0, Date.now() - startedMs);
|
|
586
|
-
}
|
|
587
|
-
function failureDelayMs(consecutiveFailures) {
|
|
588
|
-
return Math.min(2 ** (consecutiveFailures - 1), 15) * MEDIA_USAGE_CLEANUP_INTERVAL_MS;
|
|
589
|
-
}
|
|
590
|
-
function cleanupLease(leaseToken) {
|
|
591
|
-
return { leaseToken };
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
//#endregion
|
|
595
|
-
//#region src/cleanup.ts
|
|
596
|
-
/**
|
|
597
|
-
* System cleanup
|
|
598
|
-
*
|
|
599
|
-
* Runs periodic maintenance tasks that prevent unbounded accumulation of
|
|
600
|
-
* expired or stale data. Called from cron scheduler ticks and (for latency-
|
|
601
|
-
* sensitive subsystems) inline during relevant requests.
|
|
602
|
-
*
|
|
603
|
-
* Each subsystem cleanup is independent and non-fatal -- if one fails, the
|
|
604
|
-
* rest still run. Failures are logged but never surface to callers.
|
|
605
|
-
*/
|
|
606
|
-
const REVISION_KEEP_COUNT = 50;
|
|
607
|
-
const REVISION_PRUNE_BATCH_SIZE = 10;
|
|
608
|
-
/**
|
|
609
|
-
* Run all system cleanup tasks.
|
|
610
|
-
*
|
|
611
|
-
* Safe to call frequently -- each subsystem tolerates repeated calls, and
|
|
612
|
-
* repeated calls with nothing to clean are cheap.
|
|
613
|
-
*
|
|
614
|
-
* @param db - The database instance
|
|
615
|
-
* @param storage - Optional storage backend for deleting orphaned files.
|
|
616
|
-
* When omitted, pending upload DB rows are still deleted but the
|
|
617
|
-
* corresponding files in object storage are not removed.
|
|
618
|
-
*/
|
|
619
|
-
async function runSystemCleanup(db, storage) {
|
|
620
|
-
const result = {
|
|
621
|
-
challenges: -1,
|
|
622
|
-
expiredTokens: -1,
|
|
623
|
-
pendingUploads: -1,
|
|
624
|
-
pendingUploadFiles: -1,
|
|
625
|
-
uploadAttempts: -1,
|
|
626
|
-
revisionsPruned: -1,
|
|
627
|
-
mediaUsage: -1,
|
|
628
|
-
transferStaging: -1
|
|
629
|
-
};
|
|
630
|
-
try {
|
|
631
|
-
result.challenges = await cleanupExpiredChallenges(db);
|
|
632
|
-
} catch (error) {
|
|
633
|
-
console.error("[cleanup] Failed to clean expired challenges:", error);
|
|
634
|
-
}
|
|
635
|
-
try {
|
|
636
|
-
await createKyselyAdapter(db).deleteExpiredTokens();
|
|
637
|
-
result.expiredTokens = 0;
|
|
638
|
-
} catch (error) {
|
|
639
|
-
console.error("[cleanup] Failed to clean expired tokens:", error);
|
|
640
|
-
}
|
|
641
|
-
try {
|
|
642
|
-
const orphanedKeys = await new MediaRepository(db).cleanupPendingUploads();
|
|
643
|
-
result.pendingUploads = orphanedKeys.length;
|
|
644
|
-
if (storage && orphanedKeys.length > 0) {
|
|
645
|
-
let filesDeleted = 0;
|
|
646
|
-
for (const key of orphanedKeys) try {
|
|
647
|
-
await storage.delete(key);
|
|
648
|
-
filesDeleted++;
|
|
649
|
-
} catch (error) {
|
|
650
|
-
console.error(`[cleanup] Failed to delete storage file ${key}:`, error);
|
|
651
|
-
}
|
|
652
|
-
result.pendingUploadFiles = filesDeleted;
|
|
653
|
-
} else result.pendingUploadFiles = 0;
|
|
654
|
-
} catch (error) {
|
|
655
|
-
console.error("[cleanup] Failed to clean pending uploads:", error);
|
|
656
|
-
}
|
|
657
|
-
try {
|
|
658
|
-
const mediaRepo = new MediaRepository(db);
|
|
659
|
-
const completedAttemptsDeleted = await mediaRepo.deleteCompletedUploadAttempts();
|
|
660
|
-
if (!storage) result.uploadAttempts = completedAttemptsDeleted;
|
|
661
|
-
else {
|
|
662
|
-
const storageKeys = await mediaRepo.findUploadAttemptsForCleanup();
|
|
663
|
-
let attemptsDeleted = completedAttemptsDeleted;
|
|
664
|
-
for (const storageKey of storageKeys) if (await removeUploadAttempt(storage, mediaRepo, storageKey)) attemptsDeleted++;
|
|
665
|
-
result.uploadAttempts = attemptsDeleted;
|
|
666
|
-
}
|
|
667
|
-
} catch (error) {
|
|
668
|
-
console.error("[cleanup] Failed to clean media upload attempts:", error);
|
|
669
|
-
}
|
|
670
|
-
try {
|
|
671
|
-
result.revisionsPruned = await pruneQueuedRevisions(db);
|
|
672
|
-
} catch (error) {
|
|
673
|
-
console.error("[cleanup] Failed to prune revisions:", error);
|
|
674
|
-
}
|
|
675
|
-
try {
|
|
676
|
-
const mediaUsage = await cleanupMediaUsage(db);
|
|
677
|
-
result.mediaUsage = mediaUsage.status === "failed" ? -1 : mediaUsage.deletedRows;
|
|
678
|
-
} catch (error) {
|
|
679
|
-
console.error("[cleanup] Failed to clean media usage:", error);
|
|
680
|
-
}
|
|
681
|
-
if (storage) try {
|
|
682
|
-
const { collectTransferStaging } = await import("./gc-DjKY0Ay1.mjs");
|
|
683
|
-
result.transferStaging = (await collectTransferStaging(db, storage)).collected;
|
|
684
|
-
} catch (error) {
|
|
685
|
-
console.error("[transfer] Failed to collect transfer staging:", error);
|
|
686
|
-
}
|
|
687
|
-
else result.transferStaging = 0;
|
|
688
|
-
return result;
|
|
689
|
-
}
|
|
690
|
-
async function pruneQueuedRevisions(db) {
|
|
691
|
-
const queued = await db.selectFrom("_emdash_revision_prune_queue").selectAll().orderBy("revision_id").limit(REVISION_PRUNE_BATCH_SIZE).execute();
|
|
692
|
-
const revisionRepo = new RevisionRepository(db);
|
|
693
|
-
let totalPruned = 0;
|
|
694
|
-
for (const row of queued) try {
|
|
695
|
-
totalPruned += await revisionRepo.pruneQueuedEntry(row.collection, row.entry_id, row.revision_id, REVISION_KEEP_COUNT);
|
|
696
|
-
} catch (error) {
|
|
697
|
-
console.error(`[cleanup] Failed to prune revisions for ${row.collection}/${row.entry_id}:`, error);
|
|
698
|
-
}
|
|
699
|
-
return totalPruned;
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
//#endregion
|
|
703
|
-
//#region src/comments/moderator.ts
|
|
704
|
-
/** Plugin ID for the built-in default comment moderator */
|
|
705
|
-
const DEFAULT_COMMENT_MODERATOR_PLUGIN_ID = "emdash-default-comment-moderator";
|
|
706
|
-
/**
|
|
707
|
-
* The comment:moderate handler for the built-in default moderator.
|
|
708
|
-
*/
|
|
709
|
-
async function defaultCommentModerate(event, _ctx) {
|
|
710
|
-
const { comment, collectionSettings, priorApprovedCount } = event;
|
|
711
|
-
if (collectionSettings.commentsAutoApproveUsers && comment.authorUserId) return {
|
|
712
|
-
status: "approved",
|
|
713
|
-
reason: "Authenticated CMS user"
|
|
714
|
-
};
|
|
715
|
-
if (collectionSettings.commentsModeration === "none") return {
|
|
716
|
-
status: "approved",
|
|
717
|
-
reason: "Moderation disabled"
|
|
718
|
-
};
|
|
719
|
-
if (collectionSettings.commentsModeration === "first_time" && priorApprovedCount > 0) return {
|
|
720
|
-
status: "approved",
|
|
721
|
-
reason: "Returning commenter"
|
|
722
|
-
};
|
|
723
|
-
return {
|
|
724
|
-
status: "pending",
|
|
725
|
-
reason: "Held for review"
|
|
726
|
-
};
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
//#endregion
|
|
730
|
-
//#region src/scheduled-publish.ts
|
|
731
|
-
/**
|
|
732
|
-
* Default cap on items promoted per collection in a single sweep. Bounds the
|
|
733
|
-
* publish/webhook fan-out of one tick so a large backlog can't exhaust a Worker
|
|
734
|
-
* invocation's CPU/subrequest budget; the remainder drains on later ticks.
|
|
735
|
-
*/
|
|
736
|
-
const SCHEDULED_PUBLISH_BATCH_LIMIT = 100;
|
|
737
|
-
const SCHEDULED_PUBLISH_CURSOR_PREFIX = "emdash:scheduled-publish-cursor:";
|
|
738
|
-
function scheduledPublishCursorKey(collection) {
|
|
739
|
-
return `${SCHEDULED_PUBLISH_CURSOR_PREFIX}${collection}`;
|
|
740
|
-
}
|
|
741
|
-
function isScheduledPublishCursor(value) {
|
|
742
|
-
return typeof value === "object" && value !== null && "scheduledAt" in value && typeof value.scheduledAt === "string" && "id" in value && typeof value.id === "string";
|
|
743
|
-
}
|
|
744
|
-
/**
|
|
745
|
-
* Publish every content item whose `scheduled_at` is in the past.
|
|
746
|
-
*
|
|
747
|
-
* Iterates all collections, finds due items (`findReadyToPublish` returns both
|
|
748
|
-
* scheduled drafts and published entries with pending scheduled changes), and
|
|
749
|
-
* publishes each. `publish()` clears `scheduled_at`, so a second sweep is a
|
|
750
|
-
* no-op — safe to run on every tick.
|
|
751
|
-
*
|
|
752
|
-
* Bounded per collection by `limit` (default `SCHEDULED_PUBLISH_BATCH_LIMIT`):
|
|
753
|
-
* a large backlog drains across successive ticks rather than in one unbounded
|
|
754
|
-
* pass. After each collection's batch, `onPublished` (if given) is awaited so
|
|
755
|
-
* cache-tag invalidation happens incrementally, not just at the very end.
|
|
756
|
-
*
|
|
757
|
-
* Returns every item it promoted so request-less callers (the Cloudflare
|
|
758
|
-
* `scheduled()` handler) can also act on the full set.
|
|
759
|
-
*/
|
|
760
|
-
async function publishDueContent(db, options = {}) {
|
|
761
|
-
const { publish, onPublished, limit = SCHEDULED_PUBLISH_BATCH_LIMIT, currentTime = /* @__PURE__ */ new Date() } = options;
|
|
762
|
-
const published = [];
|
|
763
|
-
let collections;
|
|
764
|
-
try {
|
|
765
|
-
collections = await new SchemaRegistry(db).listCollections();
|
|
766
|
-
} catch (error) {
|
|
767
|
-
console.error("[scheduled-publish] Failed to list collections:", error);
|
|
768
|
-
return published;
|
|
769
|
-
}
|
|
770
|
-
const repo = new ContentRepository(db);
|
|
771
|
-
const optionsRepo = new OptionsRepository(db);
|
|
772
|
-
const doPublish = publish ?? ((collection, id, opts) => handleContentPublish(db, collection, id, opts));
|
|
773
|
-
const batchLimit = limit > 0 ? limit : void 0;
|
|
774
|
-
for (const collection of collections) try {
|
|
775
|
-
let cursor;
|
|
776
|
-
let cursorRevision = null;
|
|
777
|
-
if (batchLimit !== void 0) try {
|
|
778
|
-
const stored = await optionsRepo.getVersioned(scheduledPublishCursorKey(collection.slug));
|
|
779
|
-
cursorRevision = stored?.revision ?? null;
|
|
780
|
-
if (isScheduledPublishCursor(stored?.value)) cursor = stored.value;
|
|
781
|
-
} catch (error) {
|
|
782
|
-
console.error(`[scheduled-publish] Failed to read cursor for "${collection.slug}":`, error);
|
|
783
|
-
}
|
|
784
|
-
let due = await repo.findReadyToPublish(collection.slug, batchLimit, currentTime, cursor);
|
|
785
|
-
if (cursor && due.length === 0) due = await repo.findReadyToPublish(collection.slug, batchLimit, currentTime);
|
|
786
|
-
const batch = [];
|
|
787
|
-
for (const item of due) {
|
|
788
|
-
const publishedAt = item.publishedAt == null ? item.scheduledAt ?? void 0 : void 0;
|
|
789
|
-
const result = await doPublish(collection.slug, item.id, {
|
|
790
|
-
publishedAt,
|
|
791
|
-
requireScheduledDue: true,
|
|
792
|
-
expectedScheduledAt: item.scheduledAt ?? void 0,
|
|
793
|
-
currentTime
|
|
794
|
-
});
|
|
795
|
-
if (result.success) batch.push({
|
|
796
|
-
collection: collection.slug,
|
|
797
|
-
id: item.id
|
|
798
|
-
});
|
|
799
|
-
else if (result.error?.code === "NOT_DUE" || result.error?.code === "PUBLISH_REJECTED") {} else console.error(`[scheduled-publish] Failed to publish ${collection.slug}/${item.id}:`, result.error);
|
|
800
|
-
}
|
|
801
|
-
const lastAttempted = due.at(-1);
|
|
802
|
-
if (batchLimit !== void 0 && lastAttempted?.scheduledAt) try {
|
|
803
|
-
await optionsRepo.compareAndSet(scheduledPublishCursorKey(collection.slug), cursorRevision, {
|
|
804
|
-
scheduledAt: lastAttempted.scheduledAt,
|
|
805
|
-
id: lastAttempted.id
|
|
806
|
-
});
|
|
807
|
-
} catch (error) {
|
|
808
|
-
console.error(`[scheduled-publish] Failed to save cursor for "${collection.slug}":`, error);
|
|
809
|
-
}
|
|
810
|
-
if (batch.length > 0) {
|
|
811
|
-
published.push(...batch);
|
|
812
|
-
if (onPublished) try {
|
|
813
|
-
await onPublished(batch);
|
|
814
|
-
} catch (error) {
|
|
815
|
-
console.error(`[scheduled-publish] onPublished failed after "${collection.slug}" batch:`, error);
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
} catch (error) {
|
|
819
|
-
console.error(`[scheduled-publish] Sweep failed for "${collection.slug}":`, error);
|
|
820
|
-
}
|
|
821
|
-
return published;
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
//#endregion
|
|
825
|
-
//#region src/emdash-runtime.ts
|
|
826
|
-
/**
|
|
827
|
-
* EmDashRuntime - Core runtime for EmDash CMS
|
|
828
|
-
*
|
|
829
|
-
* Manages database, storage, plugins (trusted + sandboxed), hooks, and
|
|
830
|
-
* provides handlers for content/media operations.
|
|
831
|
-
*
|
|
832
|
-
* Created once per worker lifetime, cached and reused across requests.
|
|
833
|
-
*/
|
|
834
|
-
const LEADING_SLASH_PATTERN = /^\//;
|
|
835
|
-
const LOCALE_CASING_REPAIR_OPTION = "emdash:repair_locale_casing";
|
|
836
|
-
const PLUGIN_COMMENT_STATUSES = new Set([
|
|
837
|
-
"approved",
|
|
838
|
-
"pending",
|
|
839
|
-
"spam"
|
|
840
|
-
]);
|
|
841
|
-
function assertPluginCommentStatus(value, name) {
|
|
842
|
-
if (!PLUGIN_COMMENT_STATUSES.has(value)) throw Object.assign(/* @__PURE__ */ new Error(`${name} must be one of: approved, pending, spam`), { code: "COMMENT_STATUS_INVALID" });
|
|
843
|
-
}
|
|
844
|
-
function getLocaleCasingRepairVersion(locales) {
|
|
845
|
-
if (locales.length === 0) return null;
|
|
846
|
-
return `1:${locales.toSorted().join(",")}`;
|
|
847
|
-
}
|
|
848
|
-
/**
|
|
849
|
-
* Parse a JSON column expected to contain an array of strings.
|
|
850
|
-
*
|
|
851
|
-
* Throws on malformed JSON rather than returning []; callers are responsible
|
|
852
|
-
* for deciding how to handle/log the error. Empty string / null inputs return
|
|
853
|
-
* [] (they represent "no value"). Non-string array entries are filtered out.
|
|
854
|
-
*/
|
|
855
|
-
function parseStringArray(raw) {
|
|
856
|
-
if (!raw) return [];
|
|
857
|
-
const parsed = JSON.parse(raw);
|
|
858
|
-
if (!Array.isArray(parsed)) return [];
|
|
859
|
-
return parsed.filter((v) => typeof v === "string");
|
|
860
|
-
}
|
|
861
|
-
function isModerationDecision(value) {
|
|
862
|
-
if (typeof value !== "object" || value === null || !("status" in value)) return false;
|
|
863
|
-
if (value.status !== "pending" && value.status !== "approved" && value.status !== "spam") return false;
|
|
864
|
-
return !("reason" in value) || value.reason === void 0 || typeof value.reason === "string";
|
|
865
|
-
}
|
|
866
|
-
const DRAFT_ONLY_UPDATE_KEYS = new Set([
|
|
867
|
-
"data",
|
|
868
|
-
"slug",
|
|
869
|
-
"locale",
|
|
870
|
-
"skipRevision",
|
|
871
|
-
"actor",
|
|
872
|
-
"migrateBlocks",
|
|
873
|
-
"replaceBlocks"
|
|
874
|
-
]);
|
|
875
|
-
/** Field types whose schema is an array, so a stored blank string can never validate. */
|
|
876
|
-
const ARRAY_FIELD_TYPES = new Set([
|
|
877
|
-
"portableText",
|
|
878
|
-
"multiSelect",
|
|
879
|
-
"repeater"
|
|
880
|
-
]);
|
|
881
|
-
const MAX_DRAFT_STAGE_ATTEMPTS = 32;
|
|
882
|
-
const PLUGIN_INVOCATION_RELEASE_GRACE_MS = 6e4;
|
|
883
|
-
/**
|
|
884
|
-
* A `ContentSaveRejectedError` carries a message the plugin wrote for the
|
|
885
|
-
* editor; every other exception stays internal and is replaced by a generic
|
|
886
|
-
* message so hook internals cannot leak through the API.
|
|
887
|
-
*/
|
|
888
|
-
function beforeSaveFailure(error) {
|
|
889
|
-
if (isContentSaveRejection(error)) {
|
|
890
|
-
const sandbox = getSandboxSaveRejectionDetails(error);
|
|
891
|
-
if (sandbox) return {
|
|
892
|
-
success: false,
|
|
893
|
-
error: {
|
|
894
|
-
code: ErrorCode.SAVE_REJECTED,
|
|
895
|
-
message: error.message,
|
|
896
|
-
details: sandbox
|
|
897
|
-
}
|
|
898
|
-
};
|
|
899
|
-
return {
|
|
900
|
-
success: false,
|
|
901
|
-
error: {
|
|
902
|
-
code: ErrorCode.SAVE_REJECTED,
|
|
903
|
-
message: error.message
|
|
904
|
-
}
|
|
905
|
-
};
|
|
906
|
-
}
|
|
907
|
-
console.error("EmDash: content:beforeSave hook failed:", error);
|
|
908
|
-
return {
|
|
909
|
-
success: false,
|
|
910
|
-
error: {
|
|
911
|
-
code: ErrorCode.CONTENT_HOOK_ERROR,
|
|
912
|
-
message: "A plugin hook failed while saving content"
|
|
913
|
-
}
|
|
914
|
-
};
|
|
915
|
-
}
|
|
916
|
-
/**
|
|
917
|
-
* Convert a ContentItem to Record<string, unknown> for hook consumption.
|
|
918
|
-
* Hooks receive the full item as a flat record.
|
|
919
|
-
*/
|
|
920
|
-
function contentItemToRecord(item) {
|
|
921
|
-
return { ...item };
|
|
922
|
-
}
|
|
923
|
-
function toPluginContentItem(item) {
|
|
924
|
-
return {
|
|
925
|
-
id: item.id,
|
|
926
|
-
type: item.type,
|
|
927
|
-
slug: item.slug,
|
|
928
|
-
status: item.status,
|
|
929
|
-
locale: item.locale,
|
|
930
|
-
data: item.data,
|
|
931
|
-
createdAt: item.createdAt,
|
|
932
|
-
updatedAt: item.updatedAt,
|
|
933
|
-
publishedAt: item.publishedAt,
|
|
934
|
-
scheduledAt: item.scheduledAt,
|
|
935
|
-
authorId: item.authorId,
|
|
936
|
-
translationGroup: item.translationGroup,
|
|
937
|
-
liveRevisionId: item.liveRevisionId,
|
|
938
|
-
draftRevisionId: item.draftRevisionId,
|
|
939
|
-
version: item.version,
|
|
940
|
-
...item.seo === void 0 ? {} : { seo: item.seo }
|
|
941
|
-
};
|
|
942
|
-
}
|
|
943
|
-
/**
|
|
944
|
-
* Db init lock reclaim deadline. Derived from the migration race wait so
|
|
945
|
-
* they can't drift apart: a healthy init can legitimately block for the
|
|
946
|
-
* full MIGRATION_RACE_WAIT_MS inside waitForConcurrentMigrator, plus cold
|
|
947
|
-
* connect and migrator work, before it should be presumed dead. The outer
|
|
948
|
-
* runtime init lock (middleware.ts) must use a strictly larger deadline —
|
|
949
|
-
* it wraps create() → getDatabase() → this lock, and equal deadlines would
|
|
950
|
-
* let the outer reclaim while the inner is legitimately still working.
|
|
951
|
-
*/
|
|
952
|
-
const DB_INIT_DEADLINE_MS = MIGRATION_RACE_WAIT_MS + 2e4;
|
|
953
|
-
/**
|
|
954
|
-
* Db cache + its init lock live on globalThis behind a Symbol: the bundler
|
|
955
|
-
* can duplicate this module across SSR chunks (same reasoning as
|
|
956
|
-
* request-cache.ts), and a duplicated cache/lock would mean concurrent
|
|
957
|
-
* independent db inits — and duplicate migrators — per isolate.
|
|
958
|
-
*/
|
|
959
|
-
const DB_HOLDER_KEY = Symbol.for("emdash:db-cache");
|
|
960
|
-
const globalSymbolStore = globalThis;
|
|
961
|
-
function getDbHolder() {
|
|
962
|
-
let holder = globalSymbolStore[DB_HOLDER_KEY];
|
|
963
|
-
if (!holder) {
|
|
964
|
-
holder = {
|
|
965
|
-
cache: /* @__PURE__ */ new Map(),
|
|
966
|
-
lock: createInitLock(),
|
|
967
|
-
failures: /* @__PURE__ */ new Map()
|
|
968
|
-
};
|
|
969
|
-
globalSymbolStore[DB_HOLDER_KEY] = holder;
|
|
970
|
-
}
|
|
971
|
-
holder.failures ??= /* @__PURE__ */ new Map();
|
|
972
|
-
return holder;
|
|
973
|
-
}
|
|
974
|
-
/**
|
|
975
|
-
* After a database init fails (migrations threw), skip re-attempting for
|
|
976
|
-
* this long. Cold isolates still get one attempt each, so a transient
|
|
977
|
-
* failure heals on its own; a persistently failing migration is retried at
|
|
978
|
-
* most once per backoff window per isolate instead of on every request.
|
|
979
|
-
*/
|
|
980
|
-
const DB_INIT_FAILURE_BACKOFF_MS = 3e4;
|
|
981
|
-
/**
|
|
982
|
-
* Auto-seed runs at most once per isolate per database. Its lock + "done" set
|
|
983
|
-
* live on globalThis (same bundler-duplication reasoning as the db cache) so a
|
|
984
|
-
* reclaimed-and-rerun `create()` can't seed a second time concurrently. The
|
|
985
|
-
* lock polls rather than sharing a promise, so it is safe to await across a
|
|
986
|
-
* cancelled owner in workerd.
|
|
987
|
-
*/
|
|
988
|
-
const SEED_HOLDER_KEY = Symbol.for("emdash:seed-state");
|
|
989
|
-
function getSeedHolder() {
|
|
990
|
-
let holder = globalSymbolStore[SEED_HOLDER_KEY];
|
|
991
|
-
if (!holder) {
|
|
992
|
-
holder = {
|
|
993
|
-
done: /* @__PURE__ */ new Set(),
|
|
994
|
-
lock: createInitLock()
|
|
995
|
-
};
|
|
996
|
-
globalSymbolStore[SEED_HOLDER_KEY] = holder;
|
|
997
|
-
}
|
|
998
|
-
return holder;
|
|
999
|
-
}
|
|
1000
|
-
const storageCache = /* @__PURE__ */ new Map();
|
|
1001
|
-
const sandboxedPluginCache = /* @__PURE__ */ new Map();
|
|
1002
|
-
const sandboxedManifestCache = /* @__PURE__ */ new Map();
|
|
1003
|
-
/**
|
|
1004
|
-
* Per-tier sets of `${pluginId}:${version}` keys present in
|
|
1005
|
-
* `sandboxedPluginCache`. Used during sync to know which entries belong
|
|
1006
|
-
* to which install source so we can invalidate only what belongs to the
|
|
1007
|
-
* tier currently being synced.
|
|
1008
|
-
*/
|
|
1009
|
-
const marketplacePluginKeys = /* @__PURE__ */ new Set();
|
|
1010
|
-
const registryPluginKeys = /* @__PURE__ */ new Set();
|
|
1011
|
-
/**
|
|
1012
|
-
* Manifest metadata for runtime-installed sandboxed plugins (marketplace
|
|
1013
|
-
* and registry both). Keyed by `pluginId`; readers don't care which
|
|
1014
|
-
* source the plugin came from. Named `marketplace*` for legacy reasons.
|
|
1015
|
-
*/
|
|
1016
|
-
const marketplaceManifestCache = /* @__PURE__ */ new Map();
|
|
1017
|
-
/** Route metadata for sandboxed plugins: pluginId -> routeName -> RouteMeta */
|
|
1018
|
-
const sandboxedRouteMetaCache = /* @__PURE__ */ new Map();
|
|
1019
|
-
let sandboxRunner = null;
|
|
1020
|
-
function allowedBrowserImageHosts(capabilities, allowedHosts) {
|
|
1021
|
-
if (capabilities.includes("network:request:unrestricted") || capabilities.includes("network:fetch:any")) return ["*"];
|
|
1022
|
-
if (capabilities.includes("network:request") || capabilities.includes("network:fetch")) return allowedHosts;
|
|
1023
|
-
return [];
|
|
1024
|
-
}
|
|
1025
|
-
/**
|
|
1026
|
-
* EmDashRuntime - singleton per worker
|
|
1027
|
-
*/
|
|
1028
|
-
var EmDashRuntime = class EmDashRuntime {
|
|
1029
|
-
/**
|
|
1030
|
-
* The singleton database instance (worker-lifetime cached).
|
|
1031
|
-
* Use the `db` getter instead — it checks the request context first
|
|
1032
|
-
* for per-request overrides (D1 read replica sessions, DO multi-site).
|
|
1033
|
-
*/
|
|
1034
|
-
_db;
|
|
1035
|
-
storage;
|
|
1036
|
-
configuredPlugins;
|
|
1037
|
-
sandboxedPlugins;
|
|
1038
|
-
sandboxedPluginEntries;
|
|
1039
|
-
/**
|
|
1040
|
-
* Schema registry bound to the current request/event-scoped connection.
|
|
1041
|
-
* Built per access (SchemaRegistry just wraps a db) against `this.db`, the
|
|
1042
|
-
* ALS-aware getter — never a captured snapshot of the singleton. On a
|
|
1043
|
-
* connection-backed adapter (Postgres over Hyperdrive) a captured singleton
|
|
1044
|
-
* would query a socket opened by an earlier event and trip workerd's
|
|
1045
|
-
* cross-request I/O guard; the catch in handlers like handleContentUpdate
|
|
1046
|
-
* would then silently treat a revision-enabled collection as non-revisioned
|
|
1047
|
-
* and write draft edits to live columns. Same reasoning as the per-call
|
|
1048
|
-
* registry in _buildManifest().
|
|
1049
|
-
*/
|
|
1050
|
-
get schemaRegistry() {
|
|
1051
|
-
return new SchemaRegistry(this.db);
|
|
1052
|
-
}
|
|
1053
|
-
_hooks;
|
|
1054
|
-
config;
|
|
1055
|
-
mediaProviders;
|
|
1056
|
-
mediaProviderEntries;
|
|
1057
|
-
cronExecutor;
|
|
1058
|
-
email;
|
|
1059
|
-
cronScheduler;
|
|
1060
|
-
enabledPlugins;
|
|
1061
|
-
pluginStates;
|
|
1062
|
-
activePluginContentActions = /* @__PURE__ */ new Map();
|
|
1063
|
-
pendingPluginAfterHooks = /* @__PURE__ */ new Map();
|
|
1064
|
-
activePluginInvocations = /* @__PURE__ */ new Set();
|
|
1065
|
-
/** Timed-out sandbox invocations whose later actions must schedule hooks without another flush. */
|
|
1066
|
-
releasedPluginInvocations = /* @__PURE__ */ new Set();
|
|
1067
|
-
pluginInvocationCacheInvalidators = /* @__PURE__ */ new Map();
|
|
1068
|
-
pluginContentCacheInvalidator;
|
|
1069
|
-
/**
|
|
1070
|
-
* Isolate-lifetime guard so FTS indexes are verified at most once per
|
|
1071
|
-
* worker rather than on every admin request. See ensureSearchHealthy().
|
|
1072
|
-
* Uses the poison-immune single-flight cache (never a shared awaitable
|
|
1073
|
-
* promise) so a cancelled first caller can't wedge later ones.
|
|
1074
|
-
*/
|
|
1075
|
-
_searchHealthCache = createSingleFlightCache();
|
|
1076
|
-
/** Current hook pipeline. Use the `hooks` getter for external access. */
|
|
1077
|
-
get hooks() {
|
|
1078
|
-
return this._hooks;
|
|
1079
|
-
}
|
|
1080
|
-
/** All plugins eligible for the hook pipeline (includes built-in plugins).
|
|
1081
|
-
* Stored so we can rebuild the pipeline when plugins are enabled/disabled. */
|
|
1082
|
-
allPipelinePlugins;
|
|
1083
|
-
/** Guards the once-per-process plugin storage-index sync. */
|
|
1084
|
-
storageIndexesSynced = false;
|
|
1085
|
-
/** Factory options for the hook pipeline context factory */
|
|
1086
|
-
pipelineFactoryOptions;
|
|
1087
|
-
/** Dependencies needed for exclusive hook resolution */
|
|
1088
|
-
runtimeDeps;
|
|
1089
|
-
/** Mutable ref for the cron invokeCronHook closure to read the current pipeline */
|
|
1090
|
-
pipelineRef;
|
|
1091
|
-
commentModerationInProgress = /* @__PURE__ */ new Set();
|
|
1092
|
-
/**
|
|
1093
|
-
* Get the database instance for the current request.
|
|
1094
|
-
*
|
|
1095
|
-
* Checks the ALS-based request context first — middleware sets a
|
|
1096
|
-
* per-request Kysely instance there for D1 read replica sessions
|
|
1097
|
-
* or DO preview databases. Falls back to the singleton instance.
|
|
1098
|
-
*/
|
|
1099
|
-
get db() {
|
|
1100
|
-
const ctx = getRequestContext();
|
|
1101
|
-
if (ctx?.db) return ctx.db;
|
|
1102
|
-
return this._db;
|
|
1103
|
-
}
|
|
1104
|
-
/** Bind the platform cache provider used by plugin actions, including request-less cron hooks. */
|
|
1105
|
-
setPluginContentCacheInvalidator(invalidateContentCache) {
|
|
1106
|
-
this.pluginContentCacheInvalidator = invalidateContentCache;
|
|
1107
|
-
}
|
|
1108
|
-
constructor(parts) {
|
|
1109
|
-
this._db = parts.db;
|
|
1110
|
-
this.storage = parts.storage;
|
|
1111
|
-
this.configuredPlugins = parts.configuredPlugins;
|
|
1112
|
-
this.sandboxedPlugins = parts.sandboxedPlugins;
|
|
1113
|
-
this.sandboxedPluginEntries = parts.sandboxedPluginEntries;
|
|
1114
|
-
this._hooks = parts.hooks;
|
|
1115
|
-
this.enabledPlugins = parts.enabledPlugins;
|
|
1116
|
-
this.pluginStates = parts.pluginStates;
|
|
1117
|
-
this.config = parts.config;
|
|
1118
|
-
this.mediaProviders = parts.mediaProviders;
|
|
1119
|
-
this.mediaProviderEntries = parts.mediaProviderEntries;
|
|
1120
|
-
this.cronExecutor = parts.cronExecutor;
|
|
1121
|
-
this.cronScheduler = parts.cronScheduler;
|
|
1122
|
-
this.email = parts.emailPipeline;
|
|
1123
|
-
this.allPipelinePlugins = parts.allPipelinePlugins;
|
|
1124
|
-
this.pipelineFactoryOptions = parts.pipelineFactoryOptions;
|
|
1125
|
-
this.runtimeDeps = parts.runtimeDeps;
|
|
1126
|
-
this.pipelineRef = parts.pipelineRef;
|
|
1127
|
-
}
|
|
1128
|
-
/**
|
|
1129
|
-
* Get the sandbox runner instance (for marketplace install/update)
|
|
1130
|
-
*/
|
|
1131
|
-
getSandboxRunner() {
|
|
1132
|
-
return sandboxRunner;
|
|
1133
|
-
}
|
|
1134
|
-
/**
|
|
1135
|
-
* Whether the sandbox bypass mode (sandbox: false) is active.
|
|
1136
|
-
* Marketplace install/update handlers use this to skip the
|
|
1137
|
-
* SANDBOX_NOT_AVAILABLE gate, since the bypass path loads
|
|
1138
|
-
* marketplace plugins in-process via syncMarketplacePlugins().
|
|
1139
|
-
*/
|
|
1140
|
-
isSandboxBypassed() {
|
|
1141
|
-
return this.runtimeDeps.sandboxBypassed === true;
|
|
1142
|
-
}
|
|
1143
|
-
/**
|
|
1144
|
-
* Publish any content whose scheduled time has passed.
|
|
1145
|
-
* Returns the items promoted so callers can invalidate their cache tags.
|
|
1146
|
-
*/
|
|
1147
|
-
async publishScheduled() {
|
|
1148
|
-
return this.publishScheduledWithFence();
|
|
1149
|
-
}
|
|
1150
|
-
async publishScheduledWithFence(onPublished) {
|
|
1151
|
-
const recordWrite = await assertSiteWriteAllowed(this.db);
|
|
1152
|
-
const currentTime = this.runtimeDeps.now?.() ?? /* @__PURE__ */ new Date();
|
|
1153
|
-
const published = await publishDueContent(this.db, {
|
|
1154
|
-
publish: (collection, id, options) => this.handleContentPublish(collection, id, {
|
|
1155
|
-
...options,
|
|
1156
|
-
origin: { source: "scheduler" }
|
|
1157
|
-
}),
|
|
1158
|
-
onPublished,
|
|
1159
|
-
currentTime
|
|
1160
|
-
});
|
|
1161
|
-
if (published.length > 0) await recordWrite();
|
|
1162
|
-
return published;
|
|
1163
|
-
}
|
|
1164
|
-
/**
|
|
1165
|
-
* Run the full scheduled-maintenance batch: cron tasks, scheduled
|
|
1166
|
-
* publishing, and system cleanup. For request-less drivers — the
|
|
1167
|
-
* Cloudflare `scheduled()` handler invokes this from a Cron Trigger.
|
|
1168
|
-
* (On Node the timer-based scheduler drives the same work itself.)
|
|
1169
|
-
*
|
|
1170
|
-
* Each step is independent and non-fatal. Returns the content promoted
|
|
1171
|
-
* by the publishing sweep so the caller can purge edge-cache tags.
|
|
1172
|
-
*
|
|
1173
|
-
* `onPublished` (optional) is awaited after each collection's batch so a
|
|
1174
|
-
* request-less driver can invalidate edge-cache tags incrementally rather
|
|
1175
|
-
* than only after the whole sweep — bounding stale-cache exposure if the
|
|
1176
|
-
* runtime is killed mid-sweep.
|
|
1177
|
-
*/
|
|
1178
|
-
async runScheduledTasks(options = {}) {
|
|
1179
|
-
const { published } = await this.runScheduledTasksWithStats(options);
|
|
1180
|
-
return { published };
|
|
1181
|
-
}
|
|
1182
|
-
async runScheduledTasksWithStats(options = {}) {
|
|
1183
|
-
if (options.invalidateContentCache) this.pluginContentCacheInvalidator = options.invalidateContentCache;
|
|
1184
|
-
let processed = 0;
|
|
1185
|
-
if (this.cronExecutor) {
|
|
1186
|
-
try {
|
|
1187
|
-
processed = await this.cronExecutor.tick();
|
|
1188
|
-
} catch (error) {
|
|
1189
|
-
console.error("[cron] Tick failed:", error);
|
|
1190
|
-
}
|
|
1191
|
-
try {
|
|
1192
|
-
await this.cronExecutor.recoverStaleLocks();
|
|
1193
|
-
} catch (error) {
|
|
1194
|
-
console.error("[cron] Stale lock recovery failed:", error);
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
|
-
let published = [];
|
|
1198
|
-
try {
|
|
1199
|
-
published = await this.publishScheduledWithFence(options.onPublished);
|
|
1200
|
-
} catch (error) {
|
|
1201
|
-
console.error("[scheduled-publish] Sweep failed:", error);
|
|
1202
|
-
}
|
|
1203
|
-
try {
|
|
1204
|
-
await runSystemCleanup(this.db, this.storage ?? void 0);
|
|
1205
|
-
} catch (error) {
|
|
1206
|
-
console.error("[cleanup] System cleanup failed:", error);
|
|
1207
|
-
}
|
|
1208
|
-
try {
|
|
1209
|
-
await this.syncPluginStorageIndexesOnce();
|
|
1210
|
-
} catch (error) {
|
|
1211
|
-
console.error("[plugins] Storage index sync failed:", error);
|
|
1212
|
-
}
|
|
1213
|
-
await maybeRunScheduledBackup(this.db, this.storage ?? void 0);
|
|
1214
|
-
await recordSchedulerHeartbeatSafely(this.db);
|
|
1215
|
-
return {
|
|
1216
|
-
processed,
|
|
1217
|
-
published
|
|
1218
|
-
};
|
|
1219
|
-
}
|
|
1220
|
-
/**
|
|
1221
|
-
* Materialize plugin-declared storage indexes, once per process.
|
|
1222
|
-
*
|
|
1223
|
-
* Called from the scheduler path, not from request handlers — configured
|
|
1224
|
-
* plugins have no install handler, so the tick is their only sync moment.
|
|
1225
|
-
*/
|
|
1226
|
-
async syncPluginStorageIndexesOnce() {
|
|
1227
|
-
if (this.storageIndexesSynced) return;
|
|
1228
|
-
this.storageIndexesSynced = true;
|
|
1229
|
-
await syncDeclaredStorageIndexes(this.db, [...this.allPipelinePlugins, ...marketplaceManifestCache.values()]);
|
|
1230
|
-
}
|
|
1231
|
-
/**
|
|
1232
|
-
* Stop the cron scheduler gracefully.
|
|
1233
|
-
* Call during worker shutdown or hot-reload.
|
|
1234
|
-
*/
|
|
1235
|
-
async stopCron() {
|
|
1236
|
-
if (this.cronScheduler) await this.cronScheduler.stop();
|
|
1237
|
-
}
|
|
1238
|
-
/** Stop background work and discard loaded sandbox isolates. */
|
|
1239
|
-
async shutdown() {
|
|
1240
|
-
await this.stopCron();
|
|
1241
|
-
sandboxRunner?.setCommentModerate?.(null);
|
|
1242
|
-
await sandboxRunner?.terminateAll();
|
|
1243
|
-
sandboxRunner = null;
|
|
1244
|
-
sandboxedPluginCache.clear();
|
|
1245
|
-
sandboxedManifestCache.clear();
|
|
1246
|
-
sandboxedRouteMetaCache.clear();
|
|
1247
|
-
marketplaceManifestCache.clear();
|
|
1248
|
-
marketplacePluginKeys.clear();
|
|
1249
|
-
registryPluginKeys.clear();
|
|
1250
|
-
}
|
|
1251
|
-
/**
|
|
1252
|
-
* Update in-memory plugin status and rebuild the hook pipeline.
|
|
1253
|
-
*
|
|
1254
|
-
* Rebuilding the pipeline ensures disabled plugins' hooks stop firing
|
|
1255
|
-
* and re-enabled plugins' hooks start firing again without a restart.
|
|
1256
|
-
* Exclusive hook selections are re-resolved after each rebuild.
|
|
1257
|
-
*/
|
|
1258
|
-
async setPluginStatus(pluginId, status) {
|
|
1259
|
-
this.pluginStates.set(pluginId, status);
|
|
1260
|
-
if (status === "active") {
|
|
1261
|
-
this.setSandboxedPluginActive(pluginId, true);
|
|
1262
|
-
this.enabledPlugins.add(pluginId);
|
|
1263
|
-
await this.rebuildHookPipeline();
|
|
1264
|
-
await this._hooks.runPluginActivate(pluginId);
|
|
1265
|
-
} else try {
|
|
1266
|
-
await this._hooks.runPluginDeactivate(pluginId);
|
|
1267
|
-
} finally {
|
|
1268
|
-
this.setSandboxedPluginActive(pluginId, false);
|
|
1269
|
-
this.enabledPlugins.delete(pluginId);
|
|
1270
|
-
await this.rebuildHookPipeline();
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
setSandboxedPluginActive(pluginId, active) {
|
|
1274
|
-
for (const [key, plugin] of this.sandboxedPlugins) if (key.slice(0, key.lastIndexOf(":")) === pluginId) plugin.setActive?.(active);
|
|
1275
|
-
}
|
|
1276
|
-
async handlePluginEnable(pluginId) {
|
|
1277
|
-
return enableRuntimePlugin(this, pluginId);
|
|
1278
|
-
}
|
|
1279
|
-
async handlePluginDisable(pluginId) {
|
|
1280
|
-
return disableRuntimePlugin(this, pluginId);
|
|
1281
|
-
}
|
|
1282
|
-
/** Dispatch teardown lifecycle while the runtime-installed isolate is still loaded. */
|
|
1283
|
-
async runPluginUninstallLifecycle(pluginId, deleteData) {
|
|
1284
|
-
if (this.isPluginEnabled(pluginId)) try {
|
|
1285
|
-
await this._hooks.runPluginDeactivate(pluginId);
|
|
1286
|
-
} catch (error) {
|
|
1287
|
-
console.error(`EmDash: Plugin ${pluginId} deactivate hook failed during uninstall:`, error);
|
|
1288
|
-
}
|
|
1289
|
-
try {
|
|
1290
|
-
if ((await this._hooks.runPluginUninstall(pluginId, deleteData)).length > 0) return;
|
|
1291
|
-
await this.findSandboxedPlugin(pluginId)?.invokeHook("plugin:uninstall", { deleteData });
|
|
1292
|
-
} catch (error) {
|
|
1293
|
-
console.error(`EmDash: Plugin ${pluginId} uninstall hook failed:`, error);
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
async runPluginInstallLifecycle(pluginId) {
|
|
1297
|
-
await this._hooks.runPluginInstall(pluginId);
|
|
1298
|
-
await this._hooks.runPluginActivate(pluginId);
|
|
1299
|
-
}
|
|
1300
|
-
async runPluginActivateLifecycle(pluginId) {
|
|
1301
|
-
await this._hooks.runPluginActivate(pluginId);
|
|
1302
|
-
}
|
|
1303
|
-
/**
|
|
1304
|
-
* Rebuild the hook pipeline from the current set of enabled plugins.
|
|
1305
|
-
*
|
|
1306
|
-
* Filters `allPipelinePlugins` to only those in `enabledPlugins`,
|
|
1307
|
-
* creates a fresh HookPipeline, re-resolves exclusive hook selections,
|
|
1308
|
-
* and re-wires the context factory so existing references (cron
|
|
1309
|
-
* callbacks, email pipeline) use the new pipeline.
|
|
1310
|
-
*/
|
|
1311
|
-
async rebuildHookPipeline() {
|
|
1312
|
-
const newPipeline = createHookPipeline(this.allPipelinePlugins.filter((p) => this.enabledPlugins.has(p.id)), this.pipelineFactoryOptions);
|
|
1313
|
-
await EmDashRuntime.resolveExclusiveHooks(newPipeline, this.db, this.runtimeDeps);
|
|
1314
|
-
if (this.email) newPipeline.setContextFactory({ emailPipeline: this.email });
|
|
1315
|
-
newPipeline.setContextFactory({ cronReschedule: () => this.cronScheduler?.reschedule() });
|
|
1316
|
-
if (this.email) this.email.setPipeline(newPipeline);
|
|
1317
|
-
this.pipelineRef.current = newPipeline;
|
|
1318
|
-
this._hooks = newPipeline;
|
|
1319
|
-
}
|
|
1320
|
-
/**
|
|
1321
|
-
* Synchronize marketplace plugin runtime state with DB + storage.
|
|
1322
|
-
*
|
|
1323
|
-
* Ensures install/update/uninstall changes take effect immediately in the
|
|
1324
|
-
* current worker: loads newly active plugins and removes uninstalled ones.
|
|
1325
|
-
*/
|
|
1326
|
-
async syncMarketplacePlugins() {
|
|
1327
|
-
if (!this.config.marketplace) return;
|
|
1328
|
-
if (this.runtimeDeps.sandboxBypassed) {
|
|
1329
|
-
await this.syncMarketplacePluginsBypassed();
|
|
1330
|
-
return;
|
|
1331
|
-
}
|
|
1332
|
-
await this.syncSandboxedSourcePlugins("marketplace");
|
|
1333
|
-
}
|
|
1334
|
-
/**
|
|
1335
|
-
* Synchronize registry plugin runtime state with DB + storage.
|
|
1336
|
-
*
|
|
1337
|
-
* Mirrors {@link syncMarketplacePlugins} for plugins installed via the
|
|
1338
|
-
* experimental decentralized plugin registry. Called after install,
|
|
1339
|
-
* update, and uninstall handlers complete.
|
|
1340
|
-
*/
|
|
1341
|
-
async syncRegistryPlugins() {
|
|
1342
|
-
if (!getRegistryConfigInput(this.config.registry, this.config.experimental?.registry)) return;
|
|
1343
|
-
await this.syncSandboxedSourcePlugins("registry");
|
|
1344
|
-
}
|
|
1345
|
-
/**
|
|
1346
|
-
* Internal: reconcile in-memory sandboxed-plugin state with the
|
|
1347
|
-
* `_plugin_state` table for the given source tier. Shared
|
|
1348
|
-
* implementation behind {@link syncMarketplacePlugins} and
|
|
1349
|
-
* {@link syncRegistryPlugins}.
|
|
1350
|
-
*
|
|
1351
|
-
* Each source tier has its own key set in `${source}PluginKeys` so a
|
|
1352
|
-
* sync for one tier doesn't invalidate the other.
|
|
1353
|
-
*/
|
|
1354
|
-
async syncSandboxedSourcePlugins(source) {
|
|
1355
|
-
if (!this.storage) return;
|
|
1356
|
-
if (!sandboxRunner || !sandboxRunner.isAvailable()) return;
|
|
1357
|
-
const keySet = source === "marketplace" ? marketplacePluginKeys : registryPluginKeys;
|
|
1358
|
-
let pipelineChanged = false;
|
|
1359
|
-
try {
|
|
1360
|
-
const stateRepo = new PluginStateRepository(this.db);
|
|
1361
|
-
const states = source === "marketplace" ? await stateRepo.getMarketplacePlugins() : await stateRepo.getRegistryPlugins();
|
|
1362
|
-
const desired = /* @__PURE__ */ new Map();
|
|
1363
|
-
for (const state of states) {
|
|
1364
|
-
this.pluginStates.set(state.pluginId, state.status);
|
|
1365
|
-
if (state.status === "active") this.enabledPlugins.add(state.pluginId);
|
|
1366
|
-
else this.enabledPlugins.delete(state.pluginId);
|
|
1367
|
-
if (state.status !== "active") continue;
|
|
1368
|
-
const desiredVersion = source === "marketplace" ? state.marketplaceVersion ?? state.version : state.version;
|
|
1369
|
-
desired.set(state.pluginId, desiredVersion);
|
|
1370
|
-
}
|
|
1371
|
-
const keysToRemove = [];
|
|
1372
|
-
for (const key of keySet) {
|
|
1373
|
-
const [pluginId] = key.split(":");
|
|
1374
|
-
if (!pluginId) continue;
|
|
1375
|
-
const desiredVersion = desired.get(pluginId);
|
|
1376
|
-
if (desiredVersion && key === `${pluginId}:${desiredVersion}`) continue;
|
|
1377
|
-
keysToRemove.push(key);
|
|
1378
|
-
}
|
|
1379
|
-
for (const key of keysToRemove) {
|
|
1380
|
-
const [pluginId] = key.split(":");
|
|
1381
|
-
if (!pluginId) continue;
|
|
1382
|
-
if (!desired.get(pluginId)) {
|
|
1383
|
-
this.pluginStates.delete(pluginId);
|
|
1384
|
-
this.enabledPlugins.delete(pluginId);
|
|
1385
|
-
}
|
|
1386
|
-
const existing = sandboxedPluginCache.get(key);
|
|
1387
|
-
if (existing) try {
|
|
1388
|
-
await existing.terminate();
|
|
1389
|
-
} catch (error) {
|
|
1390
|
-
console.warn(`EmDash: Failed to terminate sandboxed plugin ${key}:`, error);
|
|
1391
|
-
}
|
|
1392
|
-
sandboxedPluginCache.delete(key);
|
|
1393
|
-
sandboxedManifestCache.delete(key);
|
|
1394
|
-
this.sandboxedPlugins.delete(key);
|
|
1395
|
-
this.removePluginFromLists(pluginId);
|
|
1396
|
-
keySet.delete(key);
|
|
1397
|
-
pipelineChanged = true;
|
|
1398
|
-
if (pluginId) {
|
|
1399
|
-
sandboxedRouteMetaCache.delete(pluginId);
|
|
1400
|
-
marketplaceManifestCache.delete(pluginId);
|
|
1401
|
-
}
|
|
1402
|
-
}
|
|
1403
|
-
for (const [pluginId, version] of desired) {
|
|
1404
|
-
const key = `${pluginId}:${version}`;
|
|
1405
|
-
if (sandboxedPluginCache.has(key)) {
|
|
1406
|
-
keySet.add(key);
|
|
1407
|
-
continue;
|
|
1408
|
-
}
|
|
1409
|
-
const bundle = await loadBundleFromR2(this.storage, pluginId, version, source);
|
|
1410
|
-
if (!bundle) {
|
|
1411
|
-
console.warn(`EmDash: ${source} plugin ${pluginId}@${version} not found in R2`);
|
|
1412
|
-
continue;
|
|
1413
|
-
}
|
|
1414
|
-
const loaded = await sandboxRunner.load(bundle.manifest, bundle.backendCode);
|
|
1415
|
-
sandboxedPluginCache.set(key, loaded);
|
|
1416
|
-
sandboxedManifestCache.set(key, bundle.manifest);
|
|
1417
|
-
this.sandboxedPlugins.set(key, loaded);
|
|
1418
|
-
this.allPipelinePlugins.push(createSandboxedPluginProxy(bundle.manifest, loaded));
|
|
1419
|
-
keySet.add(key);
|
|
1420
|
-
pipelineChanged = true;
|
|
1421
|
-
marketplaceManifestCache.set(pluginId, {
|
|
1422
|
-
id: bundle.manifest.id,
|
|
1423
|
-
version: bundle.manifest.version,
|
|
1424
|
-
admin: bundle.manifest.admin,
|
|
1425
|
-
mcp: bundle.manifest.mcp,
|
|
1426
|
-
storage: bundle.manifest.storage,
|
|
1427
|
-
allowedHosts: bundle.manifest.allowedHosts,
|
|
1428
|
-
capabilities: bundle.manifest.capabilities
|
|
1429
|
-
});
|
|
1430
|
-
if (bundle.manifest.routes.length > 0) {
|
|
1431
|
-
const routeMetaMap = /* @__PURE__ */ new Map();
|
|
1432
|
-
for (const entry of bundle.manifest.routes) {
|
|
1433
|
-
const normalized = normalizeManifestRoute$1(entry);
|
|
1434
|
-
routeMetaMap.set(normalized.name, buildRouteMeta(normalized));
|
|
1435
|
-
}
|
|
1436
|
-
sandboxedRouteMetaCache.set(pluginId, routeMetaMap);
|
|
1437
|
-
} else sandboxedRouteMetaCache.delete(pluginId);
|
|
1438
|
-
}
|
|
1439
|
-
if (pipelineChanged) await this.rebuildHookPipeline();
|
|
1440
|
-
} catch (error) {
|
|
1441
|
-
console.error(`EmDash: Failed to sync ${source} plugins:`, error);
|
|
1442
|
-
}
|
|
1443
|
-
}
|
|
1444
|
-
/**
|
|
1445
|
-
* Remove a plugin from the in-memory pipeline lists by ID.
|
|
1446
|
-
* Mutates allPipelinePlugins and configuredPlugins in place.
|
|
1447
|
-
*/
|
|
1448
|
-
removePluginFromLists(pluginId) {
|
|
1449
|
-
const allIdx = this.allPipelinePlugins.findIndex((p) => p.id === pluginId);
|
|
1450
|
-
if (allIdx !== -1) this.allPipelinePlugins.splice(allIdx, 1);
|
|
1451
|
-
const configIdx = this.configuredPlugins.findIndex((p) => p.id === pluginId);
|
|
1452
|
-
if (configIdx !== -1) this.configuredPlugins.splice(configIdx, 1);
|
|
1453
|
-
}
|
|
1454
|
-
/**
|
|
1455
|
-
* Sync marketplace plugin metadata in sandbox: false bypass mode.
|
|
1456
|
-
*
|
|
1457
|
-
* In bypass mode the noop runner can't load plugins, but admin pages,
|
|
1458
|
-
* widgets, and route metadata still need to refresh in-process when an
|
|
1459
|
-
* admin installs/updates/uninstalls a marketplace plugin. Otherwise the
|
|
1460
|
-
* admin UI shows stale data until the server restarts.
|
|
1461
|
-
*
|
|
1462
|
-
* Hooks and routes still won't execute under bypass (matches the
|
|
1463
|
-
* cold-start bypass behavior in loadMarketplacePluginsBypassed).
|
|
1464
|
-
*
|
|
1465
|
-
* Known limitation: bypass plugins are loaded via `import(dataUrl)`,
|
|
1466
|
-
* which Node's ESM cache keys on the full URL. Updates create fresh
|
|
1467
|
-
* module objects, but old ones remain cached for the worker's lifetime.
|
|
1468
|
-
* In practice this is a few KB per update — only matters for sites with
|
|
1469
|
-
* very frequent marketplace updates running long-lived processes. The
|
|
1470
|
-
* fix would be vm.SourceTextModule for explicit lifecycle management.
|
|
1471
|
-
*/
|
|
1472
|
-
async syncMarketplacePluginsBypassed() {
|
|
1473
|
-
if (!this.storage) return;
|
|
1474
|
-
try {
|
|
1475
|
-
const marketplaceStates = await new PluginStateRepository(this.db).getMarketplacePlugins();
|
|
1476
|
-
const desired = /* @__PURE__ */ new Map();
|
|
1477
|
-
for (const state of marketplaceStates) {
|
|
1478
|
-
this.pluginStates.set(state.pluginId, state.status);
|
|
1479
|
-
if (state.status === "active") this.enabledPlugins.add(state.pluginId);
|
|
1480
|
-
else this.enabledPlugins.delete(state.pluginId);
|
|
1481
|
-
if (state.status !== "active") continue;
|
|
1482
|
-
desired.set(state.pluginId, state.marketplaceVersion ?? state.version);
|
|
1483
|
-
}
|
|
1484
|
-
const toRemove = [];
|
|
1485
|
-
for (const pluginId of marketplaceManifestCache.keys()) if (!desired.has(pluginId)) toRemove.push(pluginId);
|
|
1486
|
-
for (const pluginId of toRemove) {
|
|
1487
|
-
const resolved = this.allPipelinePlugins.find((p) => p.id === pluginId);
|
|
1488
|
-
if (resolved) try {
|
|
1489
|
-
const deactivateHook = resolved.hooks?.["plugin:deactivate"];
|
|
1490
|
-
if (deactivateHook) {
|
|
1491
|
-
const handler = typeof deactivateHook === "function" ? deactivateHook : deactivateHook.handler;
|
|
1492
|
-
if (typeof handler === "function") await handler({ pluginId }, {});
|
|
1493
|
-
}
|
|
1494
|
-
} catch (err) {
|
|
1495
|
-
console.warn(`[emdash] plugin:deactivate hook failed for ${pluginId}:`, err);
|
|
1496
|
-
}
|
|
1497
|
-
marketplaceManifestCache.delete(pluginId);
|
|
1498
|
-
sandboxedRouteMetaCache.delete(pluginId);
|
|
1499
|
-
this.removePluginFromLists(pluginId);
|
|
1500
|
-
this.enabledPlugins.delete(pluginId);
|
|
1501
|
-
}
|
|
1502
|
-
const { adaptSandboxEntry } = await import("./plugins/adapt-sandbox-entry.mjs");
|
|
1503
|
-
const newPlugins = [];
|
|
1504
|
-
for (const [pluginId, version] of desired) {
|
|
1505
|
-
const bundle = await loadBundleFromR2(this.storage, pluginId, version);
|
|
1506
|
-
if (!bundle) {
|
|
1507
|
-
console.warn(`EmDash: Marketplace plugin ${pluginId}@${version} not found in R2`);
|
|
1508
|
-
continue;
|
|
1509
|
-
}
|
|
1510
|
-
marketplaceManifestCache.set(pluginId, {
|
|
1511
|
-
id: bundle.manifest.id,
|
|
1512
|
-
version: bundle.manifest.version,
|
|
1513
|
-
admin: bundle.manifest.admin,
|
|
1514
|
-
mcp: bundle.manifest.mcp,
|
|
1515
|
-
storage: bundle.manifest.storage,
|
|
1516
|
-
allowedHosts: bundle.manifest.allowedHosts,
|
|
1517
|
-
capabilities: bundle.manifest.capabilities
|
|
1518
|
-
});
|
|
1519
|
-
if (bundle.manifest.routes.length > 0) {
|
|
1520
|
-
const routeMetaMap = /* @__PURE__ */ new Map();
|
|
1521
|
-
for (const entry of bundle.manifest.routes) {
|
|
1522
|
-
const normalized = normalizeManifestRoute$1(entry);
|
|
1523
|
-
routeMetaMap.set(normalized.name, buildRouteMeta(normalized));
|
|
1524
|
-
}
|
|
1525
|
-
sandboxedRouteMetaCache.set(pluginId, routeMetaMap);
|
|
1526
|
-
} else sandboxedRouteMetaCache.delete(pluginId);
|
|
1527
|
-
const existing = this.allPipelinePlugins.find((p) => p.id === pluginId);
|
|
1528
|
-
if (existing && existing.version === bundle.manifest.version) continue;
|
|
1529
|
-
if (existing) this.removePluginFromLists(pluginId);
|
|
1530
|
-
try {
|
|
1531
|
-
const pluginModule = await import(`data:text/javascript;base64,${Buffer.from(bundle.backendCode).toString("base64")}`);
|
|
1532
|
-
const adapted = adaptSandboxEntry(pluginModule.default ?? pluginModule, {
|
|
1533
|
-
id: bundle.manifest.id,
|
|
1534
|
-
version: bundle.manifest.version,
|
|
1535
|
-
entrypoint: "",
|
|
1536
|
-
capabilities: bundle.manifest.capabilities ?? [],
|
|
1537
|
-
allowedHosts: bundle.manifest.allowedHosts ?? [],
|
|
1538
|
-
storage: bundle.manifest.storage ?? {},
|
|
1539
|
-
adminPages: bundle.manifest.admin?.pages,
|
|
1540
|
-
adminWidgets: bundle.manifest.admin?.widgets?.map((w) => ({
|
|
1541
|
-
id: w.id,
|
|
1542
|
-
title: w.title,
|
|
1543
|
-
size: w.size === "full" || w.size === "half" || w.size === "third" ? w.size : void 0
|
|
1544
|
-
})),
|
|
1545
|
-
settingsSchema: bundle.manifest.admin?.settingsSchema,
|
|
1546
|
-
editorPanels: bundle.manifest.admin?.editorPanels,
|
|
1547
|
-
editorActions: bundle.manifest.admin?.editorActions
|
|
1548
|
-
});
|
|
1549
|
-
newPlugins.push(adapted);
|
|
1550
|
-
this.allPipelinePlugins.push(adapted);
|
|
1551
|
-
this.configuredPlugins.push(adapted);
|
|
1552
|
-
this.enabledPlugins.add(adapted.id);
|
|
1553
|
-
} catch (error) {
|
|
1554
|
-
console.error(`EmDash: Failed to load marketplace plugin ${pluginId}@${version} in-process:`, error);
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
if (toRemove.length > 0 || newPlugins.length > 0) await this.rebuildHookPipeline();
|
|
1558
|
-
} catch (error) {
|
|
1559
|
-
console.error("EmDash: Failed to sync marketplace plugins (bypass):", error);
|
|
1560
|
-
}
|
|
1561
|
-
}
|
|
1562
|
-
/**
|
|
1563
|
-
* Create and initialize the runtime
|
|
1564
|
-
*/
|
|
1565
|
-
static async create(deps, timings) {
|
|
1566
|
-
const phase = async (name, desc, fn) => {
|
|
1567
|
-
if (!timings) return fn();
|
|
1568
|
-
const t0 = performance.now();
|
|
1569
|
-
try {
|
|
1570
|
-
return await fn();
|
|
1571
|
-
} finally {
|
|
1572
|
-
timings.push({
|
|
1573
|
-
name,
|
|
1574
|
-
dur: performance.now() - t0,
|
|
1575
|
-
desc
|
|
1576
|
-
});
|
|
1577
|
-
}
|
|
1578
|
-
};
|
|
1579
|
-
const db = await phase("rt.db", "DB init + migration policy", () => EmDashRuntime.getDatabase(deps));
|
|
1580
|
-
const resolveDb = () => {
|
|
1581
|
-
return getRequestContext()?.db ?? db;
|
|
1582
|
-
};
|
|
1583
|
-
await phase("rt.secrets", "Validate encryption key", () => validateEncryptionKeyAtStartup());
|
|
1584
|
-
const storage = EmDashRuntime.getStorage(deps);
|
|
1585
|
-
let pluginStates = /* @__PURE__ */ new Map();
|
|
1586
|
-
const configuredLocales = virtualConfig?.i18n?.locales ?? getI18nConfig()?.locales ?? [];
|
|
1587
|
-
const localeCasingRepairVersion = getLocaleCasingRepairVersion(configuredLocales);
|
|
1588
|
-
let storedLocaleCasingRepairVersion;
|
|
1589
|
-
let siteInfo;
|
|
1590
|
-
let seedGate = {
|
|
1591
|
-
collectionCount: 1,
|
|
1592
|
-
setupDone: true
|
|
1593
|
-
};
|
|
1594
|
-
const reqCtx = getRequestContext();
|
|
1595
|
-
const ownsConfiguredDb = !!deps.config.database && !(reqCtx?.dbIsIsolated && reqCtx.db);
|
|
1596
|
-
let readDb = db;
|
|
1597
|
-
let readDbDisposable;
|
|
1598
|
-
const disposeReadDb = async () => {
|
|
1599
|
-
const disposable = readDbDisposable;
|
|
1600
|
-
readDbDisposable = void 0;
|
|
1601
|
-
if (!disposable) return;
|
|
1602
|
-
try {
|
|
1603
|
-
await disposable.destroy();
|
|
1604
|
-
} catch {}
|
|
1605
|
-
};
|
|
1606
|
-
if (ownsConfiguredDb && deps.createCoalescingDialect && deps.config.database) try {
|
|
1607
|
-
const dialect = deps.createCoalescingDialect(deps.config.database.config);
|
|
1608
|
-
if (dialect) {
|
|
1609
|
-
readDb = new Kysely({
|
|
1610
|
-
dialect,
|
|
1611
|
-
log: kyselyLogOption()
|
|
1612
|
-
});
|
|
1613
|
-
readDbDisposable = readDb;
|
|
1614
|
-
}
|
|
1615
|
-
} catch {
|
|
1616
|
-
readDb = db;
|
|
1617
|
-
}
|
|
1618
|
-
const optionsRepo = new OptionsRepository(readDb);
|
|
1619
|
-
let missingManualSchemaError;
|
|
1620
|
-
const captureMissingManualSchema = (error) => {
|
|
1621
|
-
if ((deps.migrationMode ?? "auto") === "manual" && isMissingTableError(error)) missingManualSchemaError ??= error;
|
|
1622
|
-
};
|
|
1623
|
-
const readSiteInfo = async () => {
|
|
1624
|
-
const siteOpts = await optionsRepo.getMany([
|
|
1625
|
-
"emdash:site_title",
|
|
1626
|
-
"emdash:site_url",
|
|
1627
|
-
"emdash:locale",
|
|
1628
|
-
LOCALE_CASING_REPAIR_OPTION
|
|
1629
|
-
]);
|
|
1630
|
-
storedLocaleCasingRepairVersion = siteOpts.get(LOCALE_CASING_REPAIR_OPTION);
|
|
1631
|
-
return {
|
|
1632
|
-
siteName: deps.siteInfo?.name ?? siteOpts.get("emdash:site_title") ?? void 0,
|
|
1633
|
-
siteUrl: deps.siteInfo?.url ?? siteOpts.get("emdash:site_url") ?? void 0,
|
|
1634
|
-
locale: deps.siteInfo?.locale ?? siteOpts.get("emdash:locale") ?? void 0,
|
|
1635
|
-
trailingSlash: deps.siteInfo?.trailingSlash ?? virtualConfig?.trailingSlash
|
|
1636
|
-
};
|
|
1637
|
-
};
|
|
1638
|
-
const coldStartReads = [phase("rt.plugins", "Plugin states", async () => {
|
|
1639
|
-
try {
|
|
1640
|
-
const states = await readDb.selectFrom("_plugin_state").select(["plugin_id", "status"]).execute();
|
|
1641
|
-
pluginStates = new Map(states.map((s) => [s.plugin_id, s.status]));
|
|
1642
|
-
} catch (error) {
|
|
1643
|
-
captureMissingManualSchema(error);
|
|
1644
|
-
}
|
|
1645
|
-
}), phase("rt.site", "Site info options", async () => {
|
|
1646
|
-
try {
|
|
1647
|
-
siteInfo = await readSiteInfo();
|
|
1648
|
-
} catch (error) {
|
|
1649
|
-
captureMissingManualSchema(error);
|
|
1650
|
-
}
|
|
1651
|
-
})];
|
|
1652
|
-
if (ownsConfiguredDb) coldStartReads.push(phase("rt.seedcheck", "Auto-seed gate", async () => {
|
|
1653
|
-
try {
|
|
1654
|
-
const [collectionRows, setupOption] = await Promise.all([readDb.selectFrom("_emdash_collections").select("slug").execute(), readDb.selectFrom("options").select("value").where("name", "=", "emdash:setup_complete").executeTakeFirst()]);
|
|
1655
|
-
primeRegisteredCollections(collectionRows.map((row) => row.slug));
|
|
1656
|
-
const setupDone = (() => {
|
|
1657
|
-
try {
|
|
1658
|
-
return !!setupOption && JSON.parse(setupOption.value) === true;
|
|
1659
|
-
} catch {
|
|
1660
|
-
return false;
|
|
1661
|
-
}
|
|
1662
|
-
})();
|
|
1663
|
-
seedGate = {
|
|
1664
|
-
collectionCount: collectionRows.length,
|
|
1665
|
-
setupDone
|
|
1666
|
-
};
|
|
1667
|
-
} catch (error) {
|
|
1668
|
-
captureMissingManualSchema(error);
|
|
1669
|
-
}
|
|
1670
|
-
}));
|
|
1671
|
-
await Promise.all(coldStartReads);
|
|
1672
|
-
if (missingManualSchemaError) {
|
|
1673
|
-
await disposeReadDb();
|
|
1674
|
-
throw missingManualSchemaError;
|
|
1675
|
-
}
|
|
1676
|
-
if (localeCasingRepairVersion && (configuredLocales.some((locale) => locale.includes("-") || locale !== locale.toLowerCase()) || storedLocaleCasingRepairVersion !== void 0) && storedLocaleCasingRepairVersion !== localeCasingRepairVersion) await phase("rt.locale", "Repair locale casing", async () => {
|
|
1677
|
-
await repairLocaleCasing(db, configuredLocales);
|
|
1678
|
-
await new OptionsRepository(db).set(LOCALE_CASING_REPAIR_OPTION, localeCasingRepairVersion);
|
|
1679
|
-
});
|
|
1680
|
-
if (seedGate.collectionCount === 0 && !seedGate.setupDone) {
|
|
1681
|
-
try {
|
|
1682
|
-
if ((await activateMediaUsageCapture(db, { writersDrained: true })).outcome !== "active") throw new Error("Fresh-site media usage activation did not complete");
|
|
1683
|
-
} catch (error) {
|
|
1684
|
-
await disposeReadDb();
|
|
1685
|
-
throw error;
|
|
1686
|
-
}
|
|
1687
|
-
const seedKey = deps.config.database?.entrypoint ?? "default";
|
|
1688
|
-
const seedHolder = getSeedHolder();
|
|
1689
|
-
try {
|
|
1690
|
-
await initWithLock(seedHolder.lock, () => seedHolder.done.has(seedKey) ? true : void 0, async () => {
|
|
1691
|
-
const { applySeed } = await import("./apply-CoT603H3.mjs").then((n) => n.n);
|
|
1692
|
-
const { loadSeed } = await import("./load-XgJiE6_3.mjs").then((n) => n.r);
|
|
1693
|
-
const { validateSeed } = await import("./validate-CHosPEhs.mjs").then((n) => n.r);
|
|
1694
|
-
const seed = await loadSeed();
|
|
1695
|
-
if (validateSeed(seed).valid) {
|
|
1696
|
-
const seedResult = await applySeed(db, seed, { onConflict: "skip" });
|
|
1697
|
-
console.log("Auto-seeded default collections");
|
|
1698
|
-
if (seedResult.taxonomies.skipped > 0) console.warn(`[auto-seed] Kept ${seedResult.taxonomies.skipped} existing taxonomy definition(s) instead of the seed's. Edit them in the admin, or run \`emdash seed <file> --on-conflict update\` to replace them (this also overwrites other seeded records).`);
|
|
1699
|
-
}
|
|
1700
|
-
seedHolder.done.add(seedKey);
|
|
1701
|
-
return true;
|
|
1702
|
-
}, {
|
|
1703
|
-
deadlineMs: DB_INIT_DEADLINE_MS,
|
|
1704
|
-
anchor: (promise) => after(() => promise)
|
|
1705
|
-
});
|
|
1706
|
-
try {
|
|
1707
|
-
siteInfo = await readSiteInfo();
|
|
1708
|
-
} catch {}
|
|
1709
|
-
} catch {}
|
|
1710
|
-
}
|
|
1711
|
-
await disposeReadDb();
|
|
1712
|
-
const enabledPlugins = /* @__PURE__ */ new Set();
|
|
1713
|
-
for (const plugin of deps.plugins) {
|
|
1714
|
-
const status = pluginStates.get(plugin.id);
|
|
1715
|
-
if (status === void 0 || status === "active") enabledPlugins.add(plugin.id);
|
|
1716
|
-
}
|
|
1717
|
-
const allPipelinePlugins = [...deps.plugins];
|
|
1718
|
-
const bypassedPluginsList = [];
|
|
1719
|
-
if (import.meta.env.DEV) try {
|
|
1720
|
-
const devConsolePlugin = definePlugin({
|
|
1721
|
-
id: DEV_CONSOLE_EMAIL_PLUGIN_ID,
|
|
1722
|
-
version: "0.0.0",
|
|
1723
|
-
capabilities: ["hooks.email-transport:register"],
|
|
1724
|
-
hooks: { "email:deliver": {
|
|
1725
|
-
exclusive: true,
|
|
1726
|
-
handler: devConsoleEmailDeliver
|
|
1727
|
-
} }
|
|
1728
|
-
});
|
|
1729
|
-
allPipelinePlugins.push(devConsolePlugin);
|
|
1730
|
-
enabledPlugins.add(devConsolePlugin.id);
|
|
1731
|
-
} catch (error) {
|
|
1732
|
-
console.warn("[email] Failed to register dev console email provider:", error);
|
|
1733
|
-
}
|
|
1734
|
-
try {
|
|
1735
|
-
const defaultModeratorPlugin = definePlugin({
|
|
1736
|
-
id: DEFAULT_COMMENT_MODERATOR_PLUGIN_ID,
|
|
1737
|
-
version: "0.0.0",
|
|
1738
|
-
capabilities: ["users:read"],
|
|
1739
|
-
hooks: { "comment:moderate": {
|
|
1740
|
-
exclusive: true,
|
|
1741
|
-
handler: defaultCommentModerate
|
|
1742
|
-
} }
|
|
1743
|
-
});
|
|
1744
|
-
allPipelinePlugins.push(defaultModeratorPlugin);
|
|
1745
|
-
enabledPlugins.add(defaultModeratorPlugin.id);
|
|
1746
|
-
} catch (error) {
|
|
1747
|
-
console.warn("[comments] Failed to register default moderator:", error);
|
|
1748
|
-
}
|
|
1749
|
-
if (deps.sandboxBypassed && deps.sandboxedPluginEntries.length > 0) {
|
|
1750
|
-
if (typeof navigator !== "undefined" && typeof navigator.userAgent === "string" && navigator.userAgent.includes("Cloudflare-Workers")) throw new Error("sandbox: false is not supported in Cloudflare Workers. Remove the sandbox: false option or use the Cloudflare sandbox runner.");
|
|
1751
|
-
console.info("EmDash: Sandbox disabled (sandbox: false). Sandboxed plugins will run in-process without isolation.");
|
|
1752
|
-
const bypassedPlugins = await EmDashRuntime.loadBypassedPlugins(deps.sandboxedPluginEntries);
|
|
1753
|
-
for (const plugin of bypassedPlugins) {
|
|
1754
|
-
allPipelinePlugins.push(plugin);
|
|
1755
|
-
bypassedPluginsList.push(plugin);
|
|
1756
|
-
const status = pluginStates.get(plugin.id);
|
|
1757
|
-
if (status === void 0 || status === "active") enabledPlugins.add(plugin.id);
|
|
1758
|
-
}
|
|
1759
|
-
}
|
|
1760
|
-
if (deps.sandboxBypassed && deps.config.marketplace && storage) {
|
|
1761
|
-
const marketplaceBypassed = await EmDashRuntime.loadMarketplacePluginsBypassed(db, storage);
|
|
1762
|
-
for (const plugin of marketplaceBypassed) {
|
|
1763
|
-
allPipelinePlugins.push(plugin);
|
|
1764
|
-
bypassedPluginsList.push(plugin);
|
|
1765
|
-
const status = pluginStates.get(plugin.id);
|
|
1766
|
-
if (status === void 0 || status === "active") enabledPlugins.add(plugin.id);
|
|
1767
|
-
}
|
|
1768
|
-
}
|
|
1769
|
-
const sandboxedPluginPool = await phase("rt.sandbox", "Sandboxed plugins", () => EmDashRuntime.loadSandboxedPlugins(deps, db, storage, siteInfo));
|
|
1770
|
-
for (const [key, plugin] of sandboxedPluginPool) {
|
|
1771
|
-
const pluginId = key.slice(0, key.lastIndexOf(":"));
|
|
1772
|
-
const status = pluginStates.get(pluginId);
|
|
1773
|
-
plugin.setActive?.(status === void 0 || status === "active");
|
|
1774
|
-
}
|
|
1775
|
-
const installedTierPhases = [];
|
|
1776
|
-
if (deps.config.marketplace && storage && !deps.sandboxBypassed) installedTierPhases.push(phase("rt.market", "Marketplace plugins", () => EmDashRuntime.loadInstalledSandboxedPlugins("marketplace", db, storage, deps, sandboxedPluginPool, siteInfo)));
|
|
1777
|
-
if (getRegistryConfigInput(deps.config.registry, deps.config.experimental?.registry) && storage) installedTierPhases.push(phase("rt.registry", "Registry plugins", () => EmDashRuntime.loadInstalledSandboxedPlugins("registry", db, storage, deps, sandboxedPluginPool, siteInfo)));
|
|
1778
|
-
if (installedTierPhases.length > 0) await Promise.all(installedTierPhases);
|
|
1779
|
-
const configuredSandboxIds = new Set(deps.sandboxedPluginEntries.map((entry) => entry.id));
|
|
1780
|
-
const sandboxedPlugins = new Map([...sandboxedPluginPool].filter(([pluginKey]) => {
|
|
1781
|
-
const pluginId = pluginKey.slice(0, pluginKey.lastIndexOf(":"));
|
|
1782
|
-
return configuredSandboxIds.has(pluginId) || pluginStates.get(pluginId) === "active";
|
|
1783
|
-
}));
|
|
1784
|
-
for (const [pluginKey, instance] of sandboxedPlugins) {
|
|
1785
|
-
const manifest = sandboxedManifestCache.get(pluginKey);
|
|
1786
|
-
if (!manifest) continue;
|
|
1787
|
-
allPipelinePlugins.push(createSandboxedPluginProxy(manifest, instance));
|
|
1788
|
-
const status = pluginStates.get(manifest.id);
|
|
1789
|
-
if (status === void 0 || status === "active") enabledPlugins.add(manifest.id);
|
|
1790
|
-
}
|
|
1791
|
-
const runtimeRef = { current: null };
|
|
1792
|
-
const requireRuntime = () => {
|
|
1793
|
-
if (!runtimeRef.current) throw new Error("EmDash runtime is not ready");
|
|
1794
|
-
return runtimeRef.current;
|
|
1795
|
-
};
|
|
1796
|
-
const contentActions = {
|
|
1797
|
-
begin: (pluginId, invocationId, invalidateContentCache) => requireRuntime().beginPluginInvocation(pluginId, invocationId, invalidateContentCache),
|
|
1798
|
-
flush: (pluginId, invocationId, final) => requireRuntime().flushPluginAfterHooks(pluginId, invocationId, final),
|
|
1799
|
-
getVersioned: (pluginId, collection, id) => requireRuntime().pluginGetVersioned(pluginId, collection, id),
|
|
1800
|
-
publish: (pluginId, collection, id, options, invocationId, invalidateContentCache) => requireRuntime().pluginPublish(pluginId, collection, id, options, invocationId, invalidateContentCache),
|
|
1801
|
-
unpublish: (pluginId, collection, id, options, invocationId, invalidateContentCache) => requireRuntime().pluginUnpublish(pluginId, collection, id, options, invocationId, invalidateContentCache),
|
|
1802
|
-
schedule: (pluginId, collection, id, options, invocationId, invalidateContentCache) => requireRuntime().pluginSchedule(pluginId, collection, id, options, invocationId, invalidateContentCache),
|
|
1803
|
-
unschedule: (pluginId, collection, id, options, invocationId, invalidateContentCache) => requireRuntime().pluginUnschedule(pluginId, collection, id, options, invocationId, invalidateContentCache),
|
|
1804
|
-
getTrashedVersioned: (pluginId, collection, id) => requireRuntime().pluginGetTrashedVersioned(pluginId, collection, id),
|
|
1805
|
-
restore: (pluginId, collection, id, options, invocationId, invalidateContentCache) => requireRuntime().pluginRestore(pluginId, collection, id, options, invocationId, invalidateContentCache)
|
|
1806
|
-
};
|
|
1807
|
-
const pipelineFactoryOptions = {
|
|
1808
|
-
db,
|
|
1809
|
-
getDb: resolveDb,
|
|
1810
|
-
beforeContentWrite: () => assertSiteWriteAllowed(resolveDb()),
|
|
1811
|
-
contentActions,
|
|
1812
|
-
now: deps.now,
|
|
1813
|
-
storage: storage ?? void 0,
|
|
1814
|
-
siteInfo,
|
|
1815
|
-
commentModerate: (pluginId, id, status, expectedStatus) => {
|
|
1816
|
-
const current = runtimeRef.current;
|
|
1817
|
-
if (!current) throw new Error("Comment moderation is unavailable during runtime startup");
|
|
1818
|
-
return current.handlePluginCommentModerate(pluginId, id, status, expectedStatus);
|
|
1819
|
-
}
|
|
1820
|
-
};
|
|
1821
|
-
const pipeline = createHookPipeline(allPipelinePlugins.filter((p) => enabledPlugins.has(p.id)), pipelineFactoryOptions);
|
|
1822
|
-
const mediaProviders = /* @__PURE__ */ new Map();
|
|
1823
|
-
const mediaProviderEntries = deps.mediaProviderEntries ?? [];
|
|
1824
|
-
const providerContext = {
|
|
1825
|
-
db,
|
|
1826
|
-
storage,
|
|
1827
|
-
getDb: resolveDb
|
|
1828
|
-
};
|
|
1829
|
-
for (const entry of mediaProviderEntries) try {
|
|
1830
|
-
const provider = entry.createProvider(providerContext);
|
|
1831
|
-
mediaProviders.set(entry.id, provider);
|
|
1832
|
-
} catch (error) {
|
|
1833
|
-
console.warn(`Failed to initialize media provider "${entry.id}":`, error);
|
|
1834
|
-
}
|
|
1835
|
-
await phase("rt.hooks", "Exclusive hook resolution", () => EmDashRuntime.resolveExclusiveHooks(pipeline, db, deps));
|
|
1836
|
-
const emailPipeline = new EmailPipeline(pipeline);
|
|
1837
|
-
if (sandboxRunner) sandboxRunner.setEmailSend((message, pluginId) => emailPipeline.send(message, pluginId));
|
|
1838
|
-
const pipelineRef = { current: pipeline };
|
|
1839
|
-
const invokeCronHook = async (pluginId, event) => {
|
|
1840
|
-
const result = await pipelineRef.current.invokeCronHook(pluginId, event);
|
|
1841
|
-
if (!result.success && result.error) throw result.error;
|
|
1842
|
-
};
|
|
1843
|
-
pipeline.setContextFactory({ emailPipeline });
|
|
1844
|
-
let cronExecutor = null;
|
|
1845
|
-
let cronScheduler = null;
|
|
1846
|
-
await phase("rt.cron", "Cron init (recovery deferred post-response)", async () => {
|
|
1847
|
-
try {
|
|
1848
|
-
cronExecutor = new CronExecutor(resolveDb, invokeCronHook, deps.now);
|
|
1849
|
-
pipeline.setContextFactory({ cronReschedule: () => cronScheduler?.reschedule() });
|
|
1850
|
-
const executorForRecovery = cronExecutor;
|
|
1851
|
-
after(async () => {
|
|
1852
|
-
try {
|
|
1853
|
-
const recovered = await executorForRecovery.recoverStaleLocks();
|
|
1854
|
-
if (recovered > 0) console.log(`[cron] Recovered ${recovered} stale task lock(s)`);
|
|
1855
|
-
} catch (error) {
|
|
1856
|
-
console.error("[cron] Failed to recover stale task locks:", error);
|
|
1857
|
-
}
|
|
1858
|
-
});
|
|
1859
|
-
if (deps.createScheduler) {
|
|
1860
|
-
const scheduler = deps.createScheduler(cronExecutor);
|
|
1861
|
-
cronScheduler = scheduler;
|
|
1862
|
-
scheduler.setSystemCleanup(async () => {
|
|
1863
|
-
try {
|
|
1864
|
-
const runtime = runtimeRef.current;
|
|
1865
|
-
if (runtime) await runtime.publishScheduled();
|
|
1866
|
-
else {
|
|
1867
|
-
const recordWrite = await assertSiteWriteAllowed(db);
|
|
1868
|
-
if ((await publishDueContent(db)).length > 0) await recordWrite();
|
|
1869
|
-
}
|
|
1870
|
-
} catch (error) {
|
|
1871
|
-
console.error("[scheduled-publish] Sweep failed:", error);
|
|
1872
|
-
}
|
|
1873
|
-
try {
|
|
1874
|
-
await runSystemCleanup(db, storage ?? void 0);
|
|
1875
|
-
} catch (error) {
|
|
1876
|
-
console.error("[cleanup] System cleanup failed:", error);
|
|
1877
|
-
}
|
|
1878
|
-
try {
|
|
1879
|
-
await runtimeRef.current?.syncPluginStorageIndexesOnce();
|
|
1880
|
-
} catch (error) {
|
|
1881
|
-
console.error("[plugins] Storage index sync failed:", error);
|
|
1882
|
-
}
|
|
1883
|
-
await maybeRunScheduledBackup(db, storage ?? void 0);
|
|
1884
|
-
await recordSchedulerHeartbeatSafely(db);
|
|
1885
|
-
});
|
|
1886
|
-
scheduler.start();
|
|
1887
|
-
}
|
|
1888
|
-
} catch (error) {
|
|
1889
|
-
console.warn("[cron] Failed to initialize cron system:", error);
|
|
1890
|
-
}
|
|
1891
|
-
});
|
|
1892
|
-
sandboxRunner?.setCronReschedule?.(() => cronScheduler?.reschedule());
|
|
1893
|
-
const runtime = new EmDashRuntime({
|
|
1894
|
-
db,
|
|
1895
|
-
storage,
|
|
1896
|
-
configuredPlugins: [...deps.plugins, ...bypassedPluginsList],
|
|
1897
|
-
sandboxedPlugins,
|
|
1898
|
-
sandboxedPluginEntries: deps.sandboxedPluginEntries,
|
|
1899
|
-
hooks: pipeline,
|
|
1900
|
-
enabledPlugins,
|
|
1901
|
-
pluginStates,
|
|
1902
|
-
config: deps.config,
|
|
1903
|
-
mediaProviders,
|
|
1904
|
-
mediaProviderEntries,
|
|
1905
|
-
cronExecutor,
|
|
1906
|
-
cronScheduler,
|
|
1907
|
-
emailPipeline,
|
|
1908
|
-
allPipelinePlugins,
|
|
1909
|
-
pipelineFactoryOptions,
|
|
1910
|
-
runtimeDeps: deps,
|
|
1911
|
-
pipelineRef
|
|
1912
|
-
});
|
|
1913
|
-
const contentCreate = async (_pluginId, collection, input, options) => {
|
|
1914
|
-
const { seo, ...data } = input;
|
|
1915
|
-
const result = await runtime.handleContentCreate(collection, {
|
|
1916
|
-
data,
|
|
1917
|
-
seo,
|
|
1918
|
-
locale: resolveContentCreateLocale(options?.locale),
|
|
1919
|
-
translationOf: options?.translationOf
|
|
1920
|
-
}, {
|
|
1921
|
-
skipSaveHooks: options?.sandboxOrigin ? options.originHook !== void 0 : getActiveContentSaveHookName() != null,
|
|
1922
|
-
excludeAfterSavePluginId: _pluginId
|
|
1923
|
-
});
|
|
1924
|
-
if (!result.success) throw Object.assign(new Error(result.error.message), {
|
|
1925
|
-
name: result.error.code,
|
|
1926
|
-
code: result.error.code
|
|
1927
|
-
});
|
|
1928
|
-
return result.data.item;
|
|
1929
|
-
};
|
|
1930
|
-
runtime.pipelineFactoryOptions.contentCreate = contentCreate;
|
|
1931
|
-
pipeline.setContextFactory({ contentCreate });
|
|
1932
|
-
sandboxRunner?.setContentCreate?.(contentCreate);
|
|
1933
|
-
runtimeRef.current = runtime;
|
|
1934
|
-
sandboxRunner?.setCommentModerate?.((pluginId, id, status, expectedStatus) => runtime.handlePluginCommentModerate(pluginId, id, status, expectedStatus));
|
|
1935
|
-
sandboxRunner?.setContentActions?.(contentActions);
|
|
1936
|
-
return runtime;
|
|
1937
|
-
}
|
|
1938
|
-
/**
|
|
1939
|
-
* Get a media provider by ID
|
|
1940
|
-
*/
|
|
1941
|
-
getMediaProvider(providerId) {
|
|
1942
|
-
return this.mediaProviders.get(providerId);
|
|
1943
|
-
}
|
|
1944
|
-
/**
|
|
1945
|
-
* Get all media provider entries (for admin UI)
|
|
1946
|
-
*/
|
|
1947
|
-
getMediaProviderList() {
|
|
1948
|
-
return this.mediaProviderEntries.map((e) => ({
|
|
1949
|
-
id: e.id,
|
|
1950
|
-
name: e.name,
|
|
1951
|
-
icon: e.icon,
|
|
1952
|
-
capabilities: e.capabilities
|
|
1953
|
-
}));
|
|
1954
|
-
}
|
|
1955
|
-
/**
|
|
1956
|
-
* Get or create database instance
|
|
1957
|
-
*/
|
|
1958
|
-
static async getDatabase(deps) {
|
|
1959
|
-
const ctx = getRequestContext();
|
|
1960
|
-
if (ctx?.dbIsIsolated && ctx.db) return ctx.db;
|
|
1961
|
-
const dbConfig = deps.config.database;
|
|
1962
|
-
if (!dbConfig) try {
|
|
1963
|
-
return await getDb();
|
|
1964
|
-
} catch {
|
|
1965
|
-
throw new Error("EmDash database not configured. Either configure database in astro.config.mjs or use emdashLoader in live.config.ts");
|
|
1966
|
-
}
|
|
1967
|
-
const cacheKey = dbConfig.entrypoint;
|
|
1968
|
-
const holder = getDbHolder();
|
|
1969
|
-
const throwIfBackingOff = () => {
|
|
1970
|
-
const failure = holder.failures.get(cacheKey);
|
|
1971
|
-
if (!failure) return;
|
|
1972
|
-
if (Date.now() - failure.at < DB_INIT_FAILURE_BACKOFF_MS) throw new Error(`Database initialization is backing off after a recent migration failure: ${failure.message}`);
|
|
1973
|
-
holder.failures.delete(cacheKey);
|
|
1974
|
-
};
|
|
1975
|
-
throwIfBackingOff();
|
|
1976
|
-
return initWithLock(holder.lock, () => holder.cache.get(cacheKey), async (isCurrentClaim) => {
|
|
1977
|
-
throwIfBackingOff();
|
|
1978
|
-
const db = new Kysely({
|
|
1979
|
-
dialect: deps.createDialect(dbConfig.config),
|
|
1980
|
-
log: kyselyLogOption()
|
|
1981
|
-
});
|
|
1982
|
-
try {
|
|
1983
|
-
await enforceRuntimeMigrationPolicy(db, deps.migrationMode ?? "auto");
|
|
1984
|
-
} catch (error) {
|
|
1985
|
-
if (!(error instanceof ConcurrentMigrationTimeoutError) && !(error instanceof PendingMigrationsError)) holder.failures.set(cacheKey, {
|
|
1986
|
-
at: Date.now(),
|
|
1987
|
-
message: error instanceof Error ? error.message : String(error)
|
|
1988
|
-
});
|
|
1989
|
-
await db.destroy().catch(() => {});
|
|
1990
|
-
throw error;
|
|
1991
|
-
}
|
|
1992
|
-
holder.failures.delete(cacheKey);
|
|
1993
|
-
if (isCurrentClaim()) holder.cache.set(cacheKey, db);
|
|
1994
|
-
return db;
|
|
1995
|
-
}, {
|
|
1996
|
-
deadlineMs: DB_INIT_DEADLINE_MS,
|
|
1997
|
-
anchor: (promise) => after(() => promise)
|
|
1998
|
-
});
|
|
1999
|
-
}
|
|
2000
|
-
/**
|
|
2001
|
-
* Get or create storage instance
|
|
2002
|
-
*/
|
|
2003
|
-
static getStorage(deps) {
|
|
2004
|
-
const storageConfig = deps.config.storage;
|
|
2005
|
-
if (!storageConfig || !deps.createStorage) return null;
|
|
2006
|
-
const cacheKey = storageConfig.entrypoint;
|
|
2007
|
-
const cached = storageCache.get(cacheKey);
|
|
2008
|
-
if (cached) return cached;
|
|
2009
|
-
const storage = deps.createStorage(storageConfig.config);
|
|
2010
|
-
storageCache.set(cacheKey, storage);
|
|
2011
|
-
return storage;
|
|
2012
|
-
}
|
|
2013
|
-
/**
|
|
2014
|
-
* Load sandboxed plugin entries as trusted in-process plugins.
|
|
2015
|
-
* Used by the sandbox: false debugging escape hatch.
|
|
2016
|
-
*
|
|
2017
|
-
* Imports each plugin's bundled ESM code via a data URL, adapts it
|
|
2018
|
-
* with adaptSandboxEntry, and returns ResolvedPlugin objects ready
|
|
2019
|
-
* to be merged into the pipeline plugin list.
|
|
2020
|
-
*/
|
|
2021
|
-
static async loadBypassedPlugins(entries) {
|
|
2022
|
-
const { adaptSandboxEntry } = await import("./plugins/adapt-sandbox-entry.mjs");
|
|
2023
|
-
const plugins = [];
|
|
2024
|
-
for (const entry of entries) try {
|
|
2025
|
-
const pluginModule = await import(`data:text/javascript;base64,${Buffer.from(entry.code).toString("base64")}`);
|
|
2026
|
-
const pluginDef = pluginModule.default ?? pluginModule;
|
|
2027
|
-
const adminPages = entry.adminPages?.map((p) => ({
|
|
2028
|
-
path: p.path,
|
|
2029
|
-
label: p.label ?? p.path,
|
|
2030
|
-
icon: p.icon
|
|
2031
|
-
}));
|
|
2032
|
-
const adminWidgets = entry.adminWidgets?.map((w) => {
|
|
2033
|
-
const size = w.size === "full" || w.size === "half" || w.size === "third" ? w.size : void 0;
|
|
2034
|
-
return {
|
|
2035
|
-
id: w.id,
|
|
2036
|
-
title: w.title,
|
|
2037
|
-
size
|
|
2038
|
-
};
|
|
2039
|
-
});
|
|
2040
|
-
const resolved = adaptSandboxEntry(pluginDef, {
|
|
2041
|
-
id: entry.id,
|
|
2042
|
-
version: entry.version,
|
|
2043
|
-
entrypoint: "",
|
|
2044
|
-
capabilities: entry.capabilities,
|
|
2045
|
-
allowedHosts: entry.allowedHosts,
|
|
2046
|
-
storage: entry.storage,
|
|
2047
|
-
adminPages,
|
|
2048
|
-
adminWidgets,
|
|
2049
|
-
settingsSchema: entry.settingsSchema,
|
|
2050
|
-
portableTextBlocks: entry.portableTextBlocks,
|
|
2051
|
-
fieldWidgets: entry.fieldWidgets,
|
|
2052
|
-
editorPanels: entry.editorPanels,
|
|
2053
|
-
editorActions: entry.editorActions
|
|
2054
|
-
});
|
|
2055
|
-
plugins.push(resolved);
|
|
2056
|
-
console.log(`EmDash: Loaded plugin ${entry.id}:${entry.version} in-process (sandbox bypassed)`);
|
|
2057
|
-
} catch (error) {
|
|
2058
|
-
console.error(`EmDash: Failed to load sandboxed plugin ${entry.id} in-process:`, error);
|
|
2059
|
-
}
|
|
2060
|
-
return plugins;
|
|
2061
|
-
}
|
|
2062
|
-
/**
|
|
2063
|
-
* Load sandboxed plugins using SandboxRunner
|
|
2064
|
-
*/
|
|
2065
|
-
static async loadSandboxedPlugins(deps, db, mediaStorage, siteInfo) {
|
|
2066
|
-
if (!deps.sandboxEnabled) return sandboxedPluginCache;
|
|
2067
|
-
if (!sandboxRunner && deps.createSandboxRunner) sandboxRunner = deps.createSandboxRunner(createSandboxRunnerOptions({
|
|
2068
|
-
db,
|
|
2069
|
-
beforeContentWrite: () => assertSiteWriteAllowed(db),
|
|
2070
|
-
taxonomyWrite: createTaxonomyAccessWithWrite(db),
|
|
2071
|
-
now: deps.now,
|
|
2072
|
-
mediaStorage: mediaStorage ? {
|
|
2073
|
-
upload: (opts) => mediaStorage.upload({
|
|
2074
|
-
key: opts.key,
|
|
2075
|
-
body: opts.body,
|
|
2076
|
-
contentType: opts.contentType
|
|
2077
|
-
}),
|
|
2078
|
-
download: (key) => mediaStorage.download(key),
|
|
2079
|
-
delete: (key) => mediaStorage.delete(key)
|
|
2080
|
-
} : void 0
|
|
2081
|
-
}, siteInfo));
|
|
2082
|
-
if (!sandboxRunner) return sandboxedPluginCache;
|
|
2083
|
-
if (!sandboxRunner.isAvailable()) {
|
|
2084
|
-
const reason = sandboxRunner.unavailableReason?.();
|
|
2085
|
-
console.warn(reason ? `EmDash: Plugin sandbox is configured but not available on this platform: ${reason}. Sandboxed plugins will not be loaded.` : "EmDash: Plugin sandbox is configured but not available on this platform. Sandboxed plugins will not be loaded. If using @emdash-cms/sandbox-workerd/sandbox, ensure workerd is installed.");
|
|
2086
|
-
return sandboxedPluginCache;
|
|
2087
|
-
}
|
|
2088
|
-
if (deps.sandboxedPluginEntries.length === 0) return sandboxedPluginCache;
|
|
2089
|
-
if (deps.sandboxBypassed) return sandboxedPluginCache;
|
|
2090
|
-
for (const entry of deps.sandboxedPluginEntries) {
|
|
2091
|
-
const pluginKey = `${entry.id}:${entry.version}`;
|
|
2092
|
-
if (sandboxedPluginCache.has(pluginKey)) continue;
|
|
2093
|
-
try {
|
|
2094
|
-
const adminPages = entry.adminPages?.map((page) => ({
|
|
2095
|
-
path: page.path,
|
|
2096
|
-
label: page.label ?? page.path,
|
|
2097
|
-
icon: page.icon
|
|
2098
|
-
}));
|
|
2099
|
-
const adminWidgets = entry.adminWidgets?.map((widget) => ({
|
|
2100
|
-
id: widget.id,
|
|
2101
|
-
title: widget.title,
|
|
2102
|
-
size: widget.size === "full" || widget.size === "half" || widget.size === "third" ? widget.size : void 0
|
|
2103
|
-
}));
|
|
2104
|
-
const capabilities = normalizePluginCapabilities(entry.capabilities ?? []);
|
|
2105
|
-
const manifest = {
|
|
2106
|
-
id: entry.id,
|
|
2107
|
-
version: entry.version,
|
|
2108
|
-
capabilities,
|
|
2109
|
-
allowedHosts: entry.allowedHosts ?? [],
|
|
2110
|
-
storage: entry.storage ?? {},
|
|
2111
|
-
hooks: entry.hooks ?? [...HOOK_NAMES],
|
|
2112
|
-
routes: entry.routes ?? [],
|
|
2113
|
-
admin: {
|
|
2114
|
-
pages: adminPages,
|
|
2115
|
-
widgets: adminWidgets,
|
|
2116
|
-
settingsSchema: entry.settingsSchema,
|
|
2117
|
-
portableTextBlocks: entry.portableTextBlocks,
|
|
2118
|
-
fieldWidgets: entry.fieldWidgets,
|
|
2119
|
-
editorPanels: entry.editorPanels,
|
|
2120
|
-
editorActions: entry.editorActions
|
|
2121
|
-
},
|
|
2122
|
-
mcp: entry.mcp
|
|
2123
|
-
};
|
|
2124
|
-
const plugin = await sandboxRunner.load(manifest, entry.code);
|
|
2125
|
-
sandboxedPluginCache.set(pluginKey, plugin);
|
|
2126
|
-
sandboxedManifestCache.set(pluginKey, manifest);
|
|
2127
|
-
console.log(`EmDash: Loaded sandboxed plugin ${pluginKey} with capabilities: [${manifest.capabilities.join(", ")}]`);
|
|
2128
|
-
if (manifest.routes.length > 0) {
|
|
2129
|
-
const routeMetaMap = /* @__PURE__ */ new Map();
|
|
2130
|
-
for (const routeEntry of manifest.routes) {
|
|
2131
|
-
const normalized = normalizeManifestRoute$1(routeEntry);
|
|
2132
|
-
routeMetaMap.set(normalized.name, buildRouteMeta(normalized));
|
|
2133
|
-
}
|
|
2134
|
-
sandboxedRouteMetaCache.set(entry.id, routeMetaMap);
|
|
2135
|
-
} else sandboxedRouteMetaCache.delete(entry.id);
|
|
2136
|
-
} catch (error) {
|
|
2137
|
-
console.error(`EmDash: Failed to load sandboxed plugin ${entry.id}:`, error);
|
|
2138
|
-
}
|
|
2139
|
-
}
|
|
2140
|
-
return sandboxedPluginCache;
|
|
2141
|
-
}
|
|
2142
|
-
/**
|
|
2143
|
-
* Cold-start: load marketplace-installed plugins from site-local R2 storage
|
|
2144
|
-
*
|
|
2145
|
-
* Queries _plugin_state for source='marketplace' rows, fetches each bundle
|
|
2146
|
-
* from R2, and loads via SandboxRunner.
|
|
2147
|
-
*/
|
|
2148
|
-
/**
|
|
2149
|
-
* Cold-start load of all active sandboxed plugins for one install
|
|
2150
|
-
* tier (marketplace or registry) from site-local R2.
|
|
2151
|
-
*
|
|
2152
|
-
* Mirrors {@link syncSandboxedSourcePlugins} but runs once at runtime
|
|
2153
|
-
* creation, before request traffic arrives; the sync method runs on
|
|
2154
|
-
* demand after install / update / uninstall handlers.
|
|
2155
|
-
*/
|
|
2156
|
-
static async loadInstalledSandboxedPlugins(source, db, storage, deps, cache, siteInfo) {
|
|
2157
|
-
if (!sandboxRunner && deps.createSandboxRunner) sandboxRunner = deps.createSandboxRunner(createSandboxRunnerOptions({
|
|
2158
|
-
db,
|
|
2159
|
-
beforeContentWrite: () => assertSiteWriteAllowed(db),
|
|
2160
|
-
taxonomyWrite: createTaxonomyAccessWithWrite(db),
|
|
2161
|
-
now: deps.now,
|
|
2162
|
-
mediaStorage: {
|
|
2163
|
-
upload: (opts) => storage.upload({
|
|
2164
|
-
key: opts.key,
|
|
2165
|
-
body: opts.body,
|
|
2166
|
-
contentType: opts.contentType
|
|
2167
|
-
}),
|
|
2168
|
-
download: (key) => storage.download(key),
|
|
2169
|
-
delete: (key) => storage.delete(key)
|
|
2170
|
-
}
|
|
2171
|
-
}, siteInfo));
|
|
2172
|
-
if (deps.sandboxBypassed) return;
|
|
2173
|
-
if (!sandboxRunner || !sandboxRunner.isAvailable()) return;
|
|
2174
|
-
const keySet = source === "marketplace" ? marketplacePluginKeys : registryPluginKeys;
|
|
2175
|
-
try {
|
|
2176
|
-
const stateRepo = new PluginStateRepository(db);
|
|
2177
|
-
const plugins = source === "marketplace" ? await stateRepo.getMarketplacePlugins() : await stateRepo.getRegistryPlugins();
|
|
2178
|
-
for (const plugin of plugins) {
|
|
2179
|
-
if (plugin.status !== "active") continue;
|
|
2180
|
-
const version = source === "marketplace" ? plugin.marketplaceVersion ?? plugin.version : plugin.version;
|
|
2181
|
-
const pluginKey = `${plugin.pluginId}:${version}`;
|
|
2182
|
-
if (cache.has(pluginKey)) continue;
|
|
2183
|
-
try {
|
|
2184
|
-
const bundle = await loadBundleFromR2(storage, plugin.pluginId, version, source);
|
|
2185
|
-
if (!bundle) {
|
|
2186
|
-
console.warn(`EmDash: ${source} plugin ${plugin.pluginId}@${version} not found in R2`);
|
|
2187
|
-
continue;
|
|
2188
|
-
}
|
|
2189
|
-
const loaded = await sandboxRunner.load(bundle.manifest, bundle.backendCode);
|
|
2190
|
-
cache.set(pluginKey, loaded);
|
|
2191
|
-
sandboxedManifestCache.set(pluginKey, bundle.manifest);
|
|
2192
|
-
keySet.add(pluginKey);
|
|
2193
|
-
marketplaceManifestCache.set(plugin.pluginId, {
|
|
2194
|
-
id: bundle.manifest.id,
|
|
2195
|
-
version: bundle.manifest.version,
|
|
2196
|
-
admin: bundle.manifest.admin,
|
|
2197
|
-
mcp: bundle.manifest.mcp,
|
|
2198
|
-
storage: bundle.manifest.storage,
|
|
2199
|
-
allowedHosts: bundle.manifest.allowedHosts,
|
|
2200
|
-
capabilities: bundle.manifest.capabilities
|
|
2201
|
-
});
|
|
2202
|
-
if (bundle.manifest.routes.length > 0) {
|
|
2203
|
-
const routeMeta = /* @__PURE__ */ new Map();
|
|
2204
|
-
for (const entry of bundle.manifest.routes) {
|
|
2205
|
-
const normalized = normalizeManifestRoute$1(entry);
|
|
2206
|
-
routeMeta.set(normalized.name, buildRouteMeta(normalized));
|
|
2207
|
-
}
|
|
2208
|
-
sandboxedRouteMetaCache.set(plugin.pluginId, routeMeta);
|
|
2209
|
-
}
|
|
2210
|
-
console.log(`EmDash: Loaded ${source} plugin ${pluginKey} with capabilities: [${bundle.manifest.capabilities.join(", ")}]`);
|
|
2211
|
-
} catch (error) {
|
|
2212
|
-
console.error(`EmDash: Failed to load ${source} plugin ${plugin.pluginId}:`, error);
|
|
2213
|
-
}
|
|
2214
|
-
}
|
|
2215
|
-
} catch {}
|
|
2216
|
-
}
|
|
2217
|
-
/**
|
|
2218
|
-
* Cold-start: load marketplace plugins in bypass mode (sandbox: false).
|
|
2219
|
-
*
|
|
2220
|
-
* Each active marketplace bundle is read, evaluated via data URL, adapted
|
|
2221
|
-
* with adaptSandboxEntry, and returned as a ResolvedPlugin. The caller is
|
|
2222
|
-
* responsible for merging these into allPipelinePlugins / configuredPlugins
|
|
2223
|
-
* BEFORE the hook pipeline is created, so hooks and routes register in
|
|
2224
|
-
* the trusted pipeline.
|
|
2225
|
-
*
|
|
2226
|
-
* Also caches manifest and route metadata so admin UI / getManifest() work.
|
|
2227
|
-
*
|
|
2228
|
-
* Returns ResolvedPlugins to be merged into the pipeline.
|
|
2229
|
-
*/
|
|
2230
|
-
static async loadMarketplacePluginsBypassed(db, storage) {
|
|
2231
|
-
const resolved = [];
|
|
2232
|
-
try {
|
|
2233
|
-
const marketplacePlugins = await new PluginStateRepository(db).getMarketplacePlugins();
|
|
2234
|
-
if (marketplacePlugins.length === 0) return resolved;
|
|
2235
|
-
console.info("EmDash: Sandbox disabled (sandbox: false). Marketplace plugins will run in-process without isolation.");
|
|
2236
|
-
const { adaptSandboxEntry } = await import("./plugins/adapt-sandbox-entry.mjs");
|
|
2237
|
-
for (const plugin of marketplacePlugins) {
|
|
2238
|
-
if (plugin.status !== "active") continue;
|
|
2239
|
-
const version = plugin.marketplaceVersion ?? plugin.version;
|
|
2240
|
-
try {
|
|
2241
|
-
const bundle = await loadBundleFromR2(storage, plugin.pluginId, version);
|
|
2242
|
-
if (!bundle) {
|
|
2243
|
-
console.warn(`EmDash: Marketplace plugin ${plugin.pluginId}@${version} not found in R2`);
|
|
2244
|
-
continue;
|
|
2245
|
-
}
|
|
2246
|
-
marketplaceManifestCache.set(plugin.pluginId, {
|
|
2247
|
-
id: bundle.manifest.id,
|
|
2248
|
-
version: bundle.manifest.version,
|
|
2249
|
-
admin: bundle.manifest.admin,
|
|
2250
|
-
mcp: bundle.manifest.mcp,
|
|
2251
|
-
storage: bundle.manifest.storage,
|
|
2252
|
-
allowedHosts: bundle.manifest.allowedHosts,
|
|
2253
|
-
capabilities: bundle.manifest.capabilities
|
|
2254
|
-
});
|
|
2255
|
-
if (bundle.manifest.routes.length > 0) {
|
|
2256
|
-
const routeMeta = /* @__PURE__ */ new Map();
|
|
2257
|
-
for (const entry of bundle.manifest.routes) {
|
|
2258
|
-
const normalized = normalizeManifestRoute$1(entry);
|
|
2259
|
-
routeMeta.set(normalized.name, buildRouteMeta(normalized));
|
|
2260
|
-
}
|
|
2261
|
-
sandboxedRouteMetaCache.set(plugin.pluginId, routeMeta);
|
|
2262
|
-
}
|
|
2263
|
-
const pluginModule = await import(`data:text/javascript;base64,${Buffer.from(bundle.backendCode).toString("base64")}`);
|
|
2264
|
-
const adapted = adaptSandboxEntry(pluginModule.default ?? pluginModule, {
|
|
2265
|
-
id: bundle.manifest.id,
|
|
2266
|
-
version: bundle.manifest.version,
|
|
2267
|
-
entrypoint: "",
|
|
2268
|
-
capabilities: bundle.manifest.capabilities ?? [],
|
|
2269
|
-
allowedHosts: bundle.manifest.allowedHosts ?? [],
|
|
2270
|
-
storage: bundle.manifest.storage ?? {},
|
|
2271
|
-
adminPages: bundle.manifest.admin?.pages,
|
|
2272
|
-
adminWidgets: bundle.manifest.admin?.widgets?.map((w) => ({
|
|
2273
|
-
id: w.id,
|
|
2274
|
-
title: w.title,
|
|
2275
|
-
size: w.size === "full" || w.size === "half" || w.size === "third" ? w.size : void 0
|
|
2276
|
-
})),
|
|
2277
|
-
settingsSchema: bundle.manifest.admin?.settingsSchema,
|
|
2278
|
-
editorPanels: bundle.manifest.admin?.editorPanels,
|
|
2279
|
-
editorActions: bundle.manifest.admin?.editorActions
|
|
2280
|
-
});
|
|
2281
|
-
resolved.push(adapted);
|
|
2282
|
-
console.log(`EmDash: Loaded marketplace plugin ${plugin.pluginId}@${version} in-process (sandbox bypassed)`);
|
|
2283
|
-
} catch (error) {
|
|
2284
|
-
console.error(`EmDash: Failed to load marketplace plugin ${plugin.pluginId} in-process:`, error);
|
|
2285
|
-
}
|
|
2286
|
-
}
|
|
2287
|
-
} catch {}
|
|
2288
|
-
return resolved;
|
|
2289
|
-
}
|
|
2290
|
-
/**
|
|
2291
|
-
* Resolve exclusive hook selections on startup.
|
|
2292
|
-
*
|
|
2293
|
-
* Delegates to the shared resolveExclusiveHooks() in hooks.ts.
|
|
2294
|
-
* The runtime version considers all pipeline providers as "active" since
|
|
2295
|
-
* the pipeline was already built from only active/enabled plugins.
|
|
2296
|
-
*/
|
|
2297
|
-
static async resolveExclusiveHooks(pipeline, db, deps) {
|
|
2298
|
-
if (pipeline.getRegisteredExclusiveHooks().length === 0) return;
|
|
2299
|
-
let optionsRepo;
|
|
2300
|
-
try {
|
|
2301
|
-
optionsRepo = new OptionsRepository(db);
|
|
2302
|
-
} catch {
|
|
2303
|
-
return;
|
|
2304
|
-
}
|
|
2305
|
-
const preferredHints = /* @__PURE__ */ new Map();
|
|
2306
|
-
for (const entry of deps.sandboxedPluginEntries) if (entry.preferred && entry.preferred.length > 0) preferredHints.set(entry.id, entry.preferred);
|
|
2307
|
-
await resolveExclusiveHooks({
|
|
2308
|
-
pipeline,
|
|
2309
|
-
isActive: () => true,
|
|
2310
|
-
getOption: (key) => optionsRepo.get(key),
|
|
2311
|
-
getOptions: (keys) => optionsRepo.getMany(keys),
|
|
2312
|
-
setOption: (key, value) => optionsRepo.set(key, value),
|
|
2313
|
-
deleteOption: async (key) => {
|
|
2314
|
-
await optionsRepo.delete(key);
|
|
2315
|
-
},
|
|
2316
|
-
preferredHints,
|
|
2317
|
-
fallbackProviders: new Set([DEFAULT_COMMENT_MODERATOR_PLUGIN_ID])
|
|
2318
|
-
});
|
|
2319
|
-
}
|
|
2320
|
-
/**
|
|
2321
|
-
* Build the admin manifest from the live database.
|
|
2322
|
-
*
|
|
2323
|
-
* Used by the admin UI (sidebar collections, content editor field
|
|
2324
|
-
* dispatch, manifest endpoint) and by WordPress import — it's never
|
|
2325
|
-
* read on a public request, so this isn't on any anonymous hot path.
|
|
2326
|
-
*
|
|
2327
|
-
* No cross-request cache. The previous worker-isolate cache produced
|
|
2328
|
-
* a class of cross-isolate staleness bugs (#776, #873, #876, #877)
|
|
2329
|
-
* because Cloudflare Workers keeps multiple warm isolates per region
|
|
2330
|
-
* and there's no fan-out primitive to invalidate them in step. The
|
|
2331
|
-
* cache existed to amortize an N+1 schema query pattern; now that
|
|
2332
|
-
* `listCollectionsWithFields()` does the same work in two queries,
|
|
2333
|
-
* the rebuild is fast enough to pay on every admin request.
|
|
2334
|
-
*
|
|
2335
|
-
* Within a single request, `requestCached` deduplicates concurrent
|
|
2336
|
-
* callers (the manifest endpoint and an admin SSR template, say).
|
|
2337
|
-
*/
|
|
2338
|
-
getManifest() {
|
|
2339
|
-
return requestCached("emdash:manifest", () => this._buildManifest());
|
|
2340
|
-
}
|
|
2341
|
-
/**
|
|
2342
|
-
* Build the manifest from the database.
|
|
2343
|
-
*
|
|
2344
|
-
* Constant query shapes via `listCollectionsWithFields()` — one query
|
|
2345
|
-
* for collections, one batched query for fields (chunked at
|
|
2346
|
-
* `SQL_BATCH_SIZE` collection IDs to stay under D1's bound-parameter
|
|
2347
|
-
* limit). Typical sites stay well under the chunk threshold, so this
|
|
2348
|
-
* is two queries in practice; never N+1.
|
|
2349
|
-
*/
|
|
2350
|
-
async _buildManifest() {
|
|
2351
|
-
const manifestCollections = await buildManifestCollections({}, this.db);
|
|
2352
|
-
const manifestPlugins = {};
|
|
2353
|
-
for (const plugin of this.configuredPlugins) {
|
|
2354
|
-
const status = this.pluginStates.get(plugin.id);
|
|
2355
|
-
const enabled = status === void 0 || status === "active";
|
|
2356
|
-
const hasAdminEntry = !!plugin.admin?.entry;
|
|
2357
|
-
const hasAdminPages = (plugin.admin?.pages?.length ?? 0) > 0;
|
|
2358
|
-
const hasWidgets = (plugin.admin?.widgets?.length ?? 0) > 0;
|
|
2359
|
-
const hasEditorExtensions = (plugin.admin?.editorPanels?.length ?? 0) > 0 || (plugin.admin?.editorActions?.length ?? 0) > 0;
|
|
2360
|
-
let adminMode = "none";
|
|
2361
|
-
if (hasAdminEntry) adminMode = "react";
|
|
2362
|
-
else if (hasAdminPages || hasWidgets || hasEditorExtensions) adminMode = "blocks";
|
|
2363
|
-
manifestPlugins[plugin.id] = {
|
|
2364
|
-
version: plugin.version,
|
|
2365
|
-
enabled,
|
|
2366
|
-
adminMode,
|
|
2367
|
-
adminPages: plugin.admin?.pages ?? [],
|
|
2368
|
-
dashboardWidgets: plugin.admin?.widgets ?? [],
|
|
2369
|
-
portableTextBlocks: plugin.admin?.portableTextBlocks,
|
|
2370
|
-
fieldWidgets: plugin.admin?.fieldWidgets,
|
|
2371
|
-
editorPanels: plugin.admin?.editorPanels,
|
|
2372
|
-
editorActions: plugin.admin?.editorActions
|
|
2373
|
-
};
|
|
2374
|
-
}
|
|
2375
|
-
for (const entry of this.sandboxedPluginEntries) {
|
|
2376
|
-
const status = this.pluginStates.get(entry.id);
|
|
2377
|
-
const enabled = status === void 0 || status === "active";
|
|
2378
|
-
const hasAdminPages = (entry.adminPages?.length ?? 0) > 0;
|
|
2379
|
-
const hasWidgets = (entry.adminWidgets?.length ?? 0) > 0;
|
|
2380
|
-
const hasEditorExtensions = (entry.editorPanels?.length ?? 0) > 0 || (entry.editorActions?.length ?? 0) > 0;
|
|
2381
|
-
manifestPlugins[entry.id] = {
|
|
2382
|
-
version: entry.version,
|
|
2383
|
-
enabled,
|
|
2384
|
-
sandboxed: true,
|
|
2385
|
-
adminMode: hasAdminPages || hasWidgets || hasEditorExtensions ? "blocks" : "none",
|
|
2386
|
-
adminPages: entry.adminPages ?? [],
|
|
2387
|
-
dashboardWidgets: entry.adminWidgets ?? [],
|
|
2388
|
-
portableTextBlocks: entry.portableTextBlocks,
|
|
2389
|
-
fieldWidgets: entry.fieldWidgets,
|
|
2390
|
-
editorPanels: entry.editorPanels,
|
|
2391
|
-
editorActions: entry.editorActions
|
|
2392
|
-
};
|
|
2393
|
-
}
|
|
2394
|
-
for (const [pluginId, meta] of marketplaceManifestCache) {
|
|
2395
|
-
if (manifestPlugins[pluginId]) continue;
|
|
2396
|
-
const enabled = this.pluginStates.get(pluginId) === "active";
|
|
2397
|
-
const pages = meta.admin?.pages;
|
|
2398
|
-
const widgets = meta.admin?.widgets;
|
|
2399
|
-
const hasAdminPages = (pages?.length ?? 0) > 0;
|
|
2400
|
-
const hasWidgets = (widgets?.length ?? 0) > 0;
|
|
2401
|
-
const hasEditorExtensions = (meta.admin?.editorPanels?.length ?? 0) > 0 || (meta.admin?.editorActions?.length ?? 0) > 0;
|
|
2402
|
-
manifestPlugins[pluginId] = {
|
|
2403
|
-
version: meta.version,
|
|
2404
|
-
enabled,
|
|
2405
|
-
sandboxed: true,
|
|
2406
|
-
adminMode: hasAdminPages || hasWidgets || hasEditorExtensions ? "blocks" : "none",
|
|
2407
|
-
adminPages: pages ?? [],
|
|
2408
|
-
dashboardWidgets: widgets ?? [],
|
|
2409
|
-
editorPanels: meta.admin?.editorPanels,
|
|
2410
|
-
editorActions: meta.admin?.editorActions
|
|
2411
|
-
};
|
|
2412
|
-
}
|
|
2413
|
-
let manifestTaxonomies = [];
|
|
2414
|
-
let taxonomyDefinitionLocales = [];
|
|
2415
|
-
try {
|
|
2416
|
-
const rows = await selectTaxonomyDefs(this.db).orderBy("d.name").execute();
|
|
2417
|
-
taxonomyDefinitionLocales = rows.map((row) => row.locale);
|
|
2418
|
-
manifestTaxonomies = rows.map((row) => ({
|
|
2419
|
-
id: row.id,
|
|
2420
|
-
name: row.name,
|
|
2421
|
-
label: row.label,
|
|
2422
|
-
labelSingular: row.label_singular ?? void 0,
|
|
2423
|
-
hierarchical: row.hierarchical === 1,
|
|
2424
|
-
collections: parseStringArray(row.collections).toSorted(),
|
|
2425
|
-
locale: row.locale,
|
|
2426
|
-
translationGroup: row.translation_group ?? row.id
|
|
2427
|
-
}));
|
|
2428
|
-
} catch (error) {
|
|
2429
|
-
console.debug("EmDash: Could not load taxonomy definitions:", error);
|
|
2430
|
-
}
|
|
2431
|
-
try {
|
|
2432
|
-
const configuredLocales = virtualConfig?.i18n?.locales ?? getI18nConfig()?.locales ?? [];
|
|
2433
|
-
await warnAboutUnconfiguredTaxonomyLocales(this.db, configuredLocales, taxonomyDefinitionLocales);
|
|
2434
|
-
} catch (error) {
|
|
2435
|
-
console.warn("[i18n] taxonomy locale diagnostic failed:", error);
|
|
2436
|
-
}
|
|
2437
|
-
const manifestHash = await hashString(JSON.stringify(manifestCollections) + JSON.stringify(manifestPlugins) + JSON.stringify(manifestTaxonomies));
|
|
2438
|
-
const authMode = getAuthMode(this.config);
|
|
2439
|
-
const authModeValue = authMode.type === "external" ? authMode.providerType : "passkey";
|
|
2440
|
-
const i18nConfig = virtualConfig?.i18n ?? getI18nConfig();
|
|
2441
|
-
const i18n = i18nConfig && i18nConfig.locales && i18nConfig.locales.length > 1 ? {
|
|
2442
|
-
defaultLocale: i18nConfig.defaultLocale,
|
|
2443
|
-
locales: i18nConfig.locales,
|
|
2444
|
-
prefixDefaultLocale: i18nConfig.prefixDefaultLocale
|
|
2445
|
-
} : void 0;
|
|
2446
|
-
const registryConfig = resolveRegistryConfigForSandbox({
|
|
2447
|
-
registry: this.config.registry,
|
|
2448
|
-
experimentalRegistry: this.config.experimental?.registry
|
|
2449
|
-
});
|
|
2450
|
-
const { registry, error: registryConfigurationError } = resolveManifestRegistryConfig(registryConfig.input, { fieldPrefix: registryConfig.fieldPrefix });
|
|
2451
|
-
if (registryConfigurationError) console.error(`EmDash registry configuration error in ${registryConfigurationError.field} (${registryConfigurationError.code})`);
|
|
2452
|
-
return {
|
|
2453
|
-
version: VERSION,
|
|
2454
|
-
commit: COMMIT,
|
|
2455
|
-
astroVersion: this.config.astroVersion,
|
|
2456
|
-
hash: manifestHash,
|
|
2457
|
-
collections: manifestCollections,
|
|
2458
|
-
plugins: manifestPlugins,
|
|
2459
|
-
taxonomies: manifestTaxonomies,
|
|
2460
|
-
authMode: authModeValue,
|
|
2461
|
-
i18n,
|
|
2462
|
-
contentLocale: {
|
|
2463
|
-
defaultLocale: i18nConfig?.defaultLocale ?? "en",
|
|
2464
|
-
implicit: i18nConfig === null
|
|
2465
|
-
},
|
|
2466
|
-
marketplace: !!this.config.marketplace,
|
|
2467
|
-
registry,
|
|
2468
|
-
registryConfigurationError
|
|
2469
|
-
};
|
|
2470
|
-
}
|
|
2471
|
-
/**
|
|
2472
|
-
* Verify and repair FTS indexes on demand. Runs at most once per worker
|
|
2473
|
-
* lifetime.
|
|
2474
|
-
*
|
|
2475
|
-
* Originally called from `EmDashRuntime.create()`, but on a busy D1 link
|
|
2476
|
-
* (e.g. SIN replica ~80-150ms per query) it added ~1.5s to every cold
|
|
2477
|
-
* start for a modest-sized site — more than every other init phase
|
|
2478
|
-
* combined. Anonymous public reads never touch the search write path,
|
|
2479
|
-
* so the cost isn't paid back for the vast majority of requests.
|
|
2480
|
-
*
|
|
2481
|
-
* Instead, search endpoints call this lazily: the first request that
|
|
2482
|
-
* actually needs the index pays the verify cost (usually fast — no
|
|
2483
|
-
* rebuild needed), everyone else runs cold-free.
|
|
2484
|
-
*
|
|
2485
|
-
* Uses the runtime's singleton database (`this._db`) rather than the
|
|
2486
|
-
* request-scoped DB. Verify reads only, but `rebuildIndex` writes, and
|
|
2487
|
-
* a GET search request on D1 carries a `first-unconstrained` session
|
|
2488
|
-
* that's free to route at a read replica — unsafe for writes. The
|
|
2489
|
-
* singleton always goes through the default binding, which the D1
|
|
2490
|
-
* adapter will promote to `first-primary` for write statements.
|
|
2491
|
-
*
|
|
2492
|
-
* Safe to call concurrently: repeated callers share the same in-flight
|
|
2493
|
-
* promise. Errors are swallowed internally so callers don't need to
|
|
2494
|
-
* defend against FTS not existing yet (pre-setup).
|
|
2495
|
-
*/
|
|
2496
|
-
async ensureSearchHealthy() {
|
|
2497
|
-
if (!isSqlite(this._db)) return;
|
|
2498
|
-
try {
|
|
2499
|
-
await singleFlightCached(this._searchHealthCache, async () => {
|
|
2500
|
-
try {
|
|
2501
|
-
const repaired = await new FTSManager(this._db).verifyAndRepairAll();
|
|
2502
|
-
if (repaired > 0) console.log(`Repaired ${repaired} corrupted FTS index(es)`);
|
|
2503
|
-
} catch {}
|
|
2504
|
-
}, {
|
|
2505
|
-
anchor: (promise) => after(() => promise),
|
|
2506
|
-
ownerTimeoutMs: 3e4
|
|
2507
|
-
});
|
|
2508
|
-
} catch {}
|
|
2509
|
-
}
|
|
2510
|
-
async handleContentList(collection, params) {
|
|
2511
|
-
return handleContentList(this.db, collection, params);
|
|
2512
|
-
}
|
|
2513
|
-
async handleContentAuthors(collection) {
|
|
2514
|
-
return handleContentAuthors(this.db, collection);
|
|
2515
|
-
}
|
|
2516
|
-
async handleContentGet(collection, id, locale) {
|
|
2517
|
-
const result = await handleContentGet(this.db, collection, id, locale);
|
|
2518
|
-
return this.hydrateDraftData(result);
|
|
2519
|
-
}
|
|
2520
|
-
async handleContentGetIncludingTrashed(collection, id, locale) {
|
|
2521
|
-
const result = await handleContentGetIncludingTrashed(this.db, collection, id, locale);
|
|
2522
|
-
return this.hydrateDraftData(result);
|
|
2523
|
-
}
|
|
2524
|
-
/**
|
|
2525
|
-
* If the response item has a `draftRevisionId`, replace `item.data` with
|
|
2526
|
-
* the draft revision's data and expose the original published values as
|
|
2527
|
-
* `liveData`. This makes the content_get / content_update round-trip
|
|
2528
|
-
* intuitive — read returns the latest content the caller has saved
|
|
2529
|
-
* (their pending draft), with the previously-published values still
|
|
2530
|
-
* accessible for compare-style flows.
|
|
2531
|
-
*
|
|
2532
|
-
* No-op when no draft exists or the response is an error.
|
|
2533
|
-
*/
|
|
2534
|
-
async hydrateDraftData(result, options = {}) {
|
|
2535
|
-
if (!result || typeof result !== "object") return result;
|
|
2536
|
-
const r = result;
|
|
2537
|
-
if (!r.success || !r.data?.item) return result;
|
|
2538
|
-
const item = r.data.item;
|
|
2539
|
-
const draftRevisionId = typeof item.draftRevisionId === "string" ? item.draftRevisionId : null;
|
|
2540
|
-
if (!draftRevisionId) return result;
|
|
2541
|
-
try {
|
|
2542
|
-
const revision = await new RevisionRepository(this.db).findById(draftRevisionId);
|
|
2543
|
-
if (!revision) {
|
|
2544
|
-
if (options.strict) throw new Error(`Draft revision not found: ${draftRevisionId}`);
|
|
2545
|
-
return result;
|
|
2546
|
-
}
|
|
2547
|
-
const liveData = item.data && typeof item.data === "object" ? item.data : {};
|
|
2548
|
-
const revisionData = {};
|
|
2549
|
-
for (const [key, value] of Object.entries(revision.data)) if (!key.startsWith("_")) revisionData[key] = value;
|
|
2550
|
-
const mergedData = {
|
|
2551
|
-
...liveData,
|
|
2552
|
-
...revisionData
|
|
2553
|
-
};
|
|
2554
|
-
return {
|
|
2555
|
-
...result,
|
|
2556
|
-
data: {
|
|
2557
|
-
...r.data,
|
|
2558
|
-
item: {
|
|
2559
|
-
...item,
|
|
2560
|
-
...options.includeStagedSlug && typeof revision.data._slug === "string" ? { slug: revision.data._slug } : {},
|
|
2561
|
-
data: mergedData,
|
|
2562
|
-
liveData
|
|
2563
|
-
}
|
|
2564
|
-
}
|
|
2565
|
-
};
|
|
2566
|
-
} catch (error) {
|
|
2567
|
-
if (options.strict) throw error;
|
|
2568
|
-
console.error("[emdash] draft hydration failed:", error);
|
|
2569
|
-
return result;
|
|
2570
|
-
}
|
|
2571
|
-
}
|
|
2572
|
-
async handleContentCreate(collection, body, options = {}) {
|
|
2573
|
-
const actor = body.actor ? { ...body.actor } : void 0;
|
|
2574
|
-
let locale;
|
|
2575
|
-
try {
|
|
2576
|
-
locale = resolveContentCreateLocale(body.locale);
|
|
2577
|
-
} catch (error) {
|
|
2578
|
-
return {
|
|
2579
|
-
success: false,
|
|
2580
|
-
error: {
|
|
2581
|
-
code: "VALIDATION_ERROR",
|
|
2582
|
-
message: error instanceof Error ? error.message : "Invalid locale"
|
|
2583
|
-
}
|
|
2584
|
-
};
|
|
2585
|
-
}
|
|
2586
|
-
let translationSource = null;
|
|
2587
|
-
let sharedFieldSlugs = [];
|
|
2588
|
-
if (body.translationOf) {
|
|
2589
|
-
const repo = new ContentRepository(this.db);
|
|
2590
|
-
const source = await repo.findById(collection, body.translationOf);
|
|
2591
|
-
if (!source) return {
|
|
2592
|
-
success: false,
|
|
2593
|
-
error: {
|
|
2594
|
-
code: "NOT_FOUND",
|
|
2595
|
-
message: "Translation source content not found"
|
|
2596
|
-
}
|
|
2597
|
-
};
|
|
2598
|
-
translationSource = source;
|
|
2599
|
-
sharedFieldSlugs = (await this.db.selectFrom("_emdash_fields as field").innerJoin("_emdash_collections as collection", "collection.id", "field.collection_id").select("field.slug").where("collection.slug", "=", collection).where("field.translatable", "=", 0).execute()).map((field) => field.slug);
|
|
2600
|
-
if ((await repo.findTranslations(collection, source.translationGroup ?? source.id)).some((sibling) => sibling.locale?.toLowerCase() === locale.toLowerCase())) return {
|
|
2601
|
-
success: false,
|
|
2602
|
-
error: {
|
|
2603
|
-
code: "CONFLICT",
|
|
2604
|
-
message: `Translation already exists in locale "${locale}" for this content item`
|
|
2605
|
-
}
|
|
2606
|
-
};
|
|
2607
|
-
}
|
|
2608
|
-
let processedData = body.data;
|
|
2609
|
-
if (!options.skipSaveHooks && this.hooks.hasHooks("content:beforeSave")) try {
|
|
2610
|
-
processedData = (await this.hooks.runContentBeforeSave(body.data, collection, true, void 0, actor)).content;
|
|
2611
|
-
} catch (error) {
|
|
2612
|
-
return beforeSaveFailure(error);
|
|
2613
|
-
}
|
|
2614
|
-
if (translationSource) {
|
|
2615
|
-
processedData = { ...processedData };
|
|
2616
|
-
for (const slug of sharedFieldSlugs) if (Object.hasOwn(translationSource.data, slug)) processedData[slug] = translationSource.data[slug];
|
|
2617
|
-
else delete processedData[slug];
|
|
2618
|
-
}
|
|
2619
|
-
const collectionInfo = await this.schemaRegistry.getCollectionWithFields(collection).catch(() => null);
|
|
2620
|
-
const resolvedBlockTypes = collectionInfo?.fields.some((field) => field.type === "blocks") ? await resolveBlockTypes(this.db) : void 0;
|
|
2621
|
-
if (collectionInfo) try {
|
|
2622
|
-
processedData = await normalizeBlocksData(this.db, collectionInfo, processedData, translationSource?.data, {
|
|
2623
|
-
migrateBlocks: body.migrateBlocks,
|
|
2624
|
-
replaceBlocks: body.replaceBlocks
|
|
2625
|
-
}, false, resolvedBlockTypes);
|
|
2626
|
-
} catch (error) {
|
|
2627
|
-
if (error instanceof SchemaError) return {
|
|
2628
|
-
success: false,
|
|
2629
|
-
error: {
|
|
2630
|
-
code: error.code,
|
|
2631
|
-
message: error.message,
|
|
2632
|
-
details: error.details
|
|
2633
|
-
}
|
|
2634
|
-
};
|
|
2635
|
-
throw error;
|
|
2636
|
-
}
|
|
2637
|
-
processedData = await this.normalizeFieldValues(collection, processedData, collectionInfo, resolvedBlockTypes);
|
|
2638
|
-
const { validateContentData } = await import("./validation-BFecQczG.mjs");
|
|
2639
|
-
const validation = await validateContentData(this.db, collection, processedData, { partial: false });
|
|
2640
|
-
if (!validation.ok) return {
|
|
2641
|
-
success: false,
|
|
2642
|
-
error: validation.error
|
|
2643
|
-
};
|
|
2644
|
-
const { actor: _discardedActor, migrateBlocks: _discardedMigrateBlocks, replaceBlocks: _discardedReplaceBlocks, ...contentBody } = body;
|
|
2645
|
-
const result = await handleContentCreate(this.db, collection, {
|
|
2646
|
-
...contentBody,
|
|
2647
|
-
data: processedData,
|
|
2648
|
-
locale,
|
|
2649
|
-
authorId: body.authorId,
|
|
2650
|
-
bylines: body.bylines
|
|
2651
|
-
});
|
|
2652
|
-
if (result.success && result.data) await this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.item.id]);
|
|
2653
|
-
if (!options.skipSaveHooks && result.success && result.data) this.runAfterSaveHooks(contentItemToRecord(result.data.item), collection, true, actor, options.excludeAfterSavePluginId);
|
|
2654
|
-
return result;
|
|
2655
|
-
}
|
|
2656
|
-
async handleContentUpdate(collection, id, body) {
|
|
2657
|
-
const actor = body.actor ? { ...body.actor } : void 0;
|
|
2658
|
-
const repo = new ContentRepository(this.db);
|
|
2659
|
-
const resolvedItem = await repo.findByIdOrSlug(collection, id, body.locale);
|
|
2660
|
-
const resolvedId = resolvedItem?.id ?? id;
|
|
2661
|
-
if (body._rev) {
|
|
2662
|
-
if (!resolvedItem) return {
|
|
2663
|
-
success: false,
|
|
2664
|
-
error: {
|
|
2665
|
-
code: "NOT_FOUND",
|
|
2666
|
-
message: `Content item not found: ${id}`
|
|
2667
|
-
}
|
|
2668
|
-
};
|
|
2669
|
-
const revCheck = validateRev(body._rev, resolvedItem);
|
|
2670
|
-
if (!revCheck.valid) return {
|
|
2671
|
-
success: false,
|
|
2672
|
-
error: {
|
|
2673
|
-
code: "CONFLICT",
|
|
2674
|
-
message: revCheck.message
|
|
2675
|
-
}
|
|
2676
|
-
};
|
|
2677
|
-
}
|
|
2678
|
-
const { _rev: _discardedRev, actor: _discardedActor, migrateBlocks, replaceBlocks, ...bodyWithoutRev } = body;
|
|
2679
|
-
const blockWriteOptions = {
|
|
2680
|
-
migrateBlocks,
|
|
2681
|
-
replaceBlocks
|
|
2682
|
-
};
|
|
2683
|
-
const collectionInfo = bodyWithoutRev.data ? await this.schemaRegistry.getCollectionWithFields(collection).catch(() => null) : null;
|
|
2684
|
-
const knownFieldSlugs = new Set((collectionInfo?.fields ?? []).map((f) => f.slug));
|
|
2685
|
-
let processedData = bodyWithoutRev.data;
|
|
2686
|
-
let resolvedBlockTypes;
|
|
2687
|
-
if (bodyWithoutRev.data) {
|
|
2688
|
-
if (this.hooks.hasHooks("content:beforeSave")) try {
|
|
2689
|
-
processedData = (await this.hooks.runContentBeforeSave(bodyWithoutRev.data, collection, false, resolvedItem?.id, actor)).content;
|
|
2690
|
-
} catch (error) {
|
|
2691
|
-
return beforeSaveFailure(error);
|
|
2692
|
-
}
|
|
2693
|
-
processedData = await this.normalizeFieldValues(collection, processedData, collectionInfo, void 0, false);
|
|
2694
|
-
if (collectionInfo?.fields) processedData = await this.dropUnknownKeysAlreadyStored(processedData, resolvedItem, knownFieldSlugs);
|
|
2695
|
-
if (collectionInfo?.fields.some((field) => field.type === "blocks" && Object.hasOwn(processedData, field.slug))) resolvedBlockTypes = await resolveBlockTypes(this.db);
|
|
2696
|
-
if (!collectionInfo?.supports?.includes("revisions")) {
|
|
2697
|
-
if (collectionInfo) try {
|
|
2698
|
-
processedData = await normalizeBlocksData(this.db, collectionInfo, processedData, resolvedItem?.data, blockWriteOptions, true, resolvedBlockTypes);
|
|
2699
|
-
processedData = await this.normalizeFieldValues(collection, processedData, collectionInfo, resolvedBlockTypes);
|
|
2700
|
-
} catch (error) {
|
|
2701
|
-
if (error instanceof SchemaError) return {
|
|
2702
|
-
success: false,
|
|
2703
|
-
error: {
|
|
2704
|
-
code: error.code,
|
|
2705
|
-
message: error.message,
|
|
2706
|
-
details: error.details
|
|
2707
|
-
}
|
|
2708
|
-
};
|
|
2709
|
-
throw error;
|
|
2710
|
-
}
|
|
2711
|
-
const { validateContentData } = await import("./validation-BFecQczG.mjs");
|
|
2712
|
-
const validation = await validateContentData(this.db, collection, processedData, { partial: true });
|
|
2713
|
-
if (!validation.ok) return {
|
|
2714
|
-
success: false,
|
|
2715
|
-
error: validation.error
|
|
2716
|
-
};
|
|
2717
|
-
}
|
|
2718
|
-
}
|
|
2719
|
-
let usesDraftRevisions = false;
|
|
2720
|
-
let draftStorageChanged = false;
|
|
2721
|
-
if (processedData) {
|
|
2722
|
-
if (collectionInfo?.supports?.includes("revisions")) {
|
|
2723
|
-
usesDraftRevisions = true;
|
|
2724
|
-
const revisionRepo = new RevisionRepository(this.db);
|
|
2725
|
-
let existing = await repo.findById(collection, resolvedId);
|
|
2726
|
-
for (let attempt = 0; existing && attempt < MAX_DRAFT_STAGE_ATTEMPTS; attempt++) {
|
|
2727
|
-
let baseData;
|
|
2728
|
-
if (existing.draftRevisionId) baseData = (await revisionRepo.findById(existing.draftRevisionId))?.data ?? existing.data;
|
|
2729
|
-
else baseData = existing.data;
|
|
2730
|
-
let attemptData = processedData;
|
|
2731
|
-
try {
|
|
2732
|
-
attemptData = await normalizeBlocksData(this.db, collectionInfo, processedData, baseData, blockWriteOptions, true, resolvedBlockTypes);
|
|
2733
|
-
attemptData = await this.normalizeFieldValues(collection, attemptData, collectionInfo, resolvedBlockTypes);
|
|
2734
|
-
} catch (error) {
|
|
2735
|
-
if (error instanceof SchemaError) return {
|
|
2736
|
-
success: false,
|
|
2737
|
-
error: {
|
|
2738
|
-
code: error.code,
|
|
2739
|
-
message: error.message,
|
|
2740
|
-
details: error.details
|
|
2741
|
-
}
|
|
2742
|
-
};
|
|
2743
|
-
throw error;
|
|
2744
|
-
}
|
|
2745
|
-
const { validateContentData } = await import("./validation-BFecQczG.mjs");
|
|
2746
|
-
const validation = await validateContentData(this.db, collection, attemptData, { partial: true });
|
|
2747
|
-
if (!validation.ok) return {
|
|
2748
|
-
success: false,
|
|
2749
|
-
error: validation.error
|
|
2750
|
-
};
|
|
2751
|
-
const mergedData = collectionInfo?.fields ? keepKnownFields({
|
|
2752
|
-
...baseData,
|
|
2753
|
-
...attemptData
|
|
2754
|
-
}, knownFieldSlugs) : {
|
|
2755
|
-
...baseData,
|
|
2756
|
-
...attemptData
|
|
2757
|
-
};
|
|
2758
|
-
if (bodyWithoutRev.slug !== void 0) mergedData._slug = bodyWithoutRev.slug;
|
|
2759
|
-
const revision = await revisionRepo.create({
|
|
2760
|
-
collection,
|
|
2761
|
-
entryId: resolvedId,
|
|
2762
|
-
data: mergedData,
|
|
2763
|
-
authorId: actor?.id
|
|
2764
|
-
});
|
|
2765
|
-
let staged;
|
|
2766
|
-
try {
|
|
2767
|
-
staged = await repo.replaceDraftRevision(collection, resolvedId, revision.id, existing);
|
|
2768
|
-
} catch (error) {
|
|
2769
|
-
try {
|
|
2770
|
-
await revisionRepo.deleteIfUnreferenced(collection, resolvedId, revision.id);
|
|
2771
|
-
} catch (cleanupError) {
|
|
2772
|
-
console.error(`[emdash] Failed to clean up unstaged revision ${revision.id}:`, cleanupError);
|
|
2773
|
-
}
|
|
2774
|
-
throw error;
|
|
2775
|
-
}
|
|
2776
|
-
if (!staged) {
|
|
2777
|
-
try {
|
|
2778
|
-
await revisionRepo.deleteIfUnreferenced(collection, resolvedId, revision.id);
|
|
2779
|
-
} catch (cleanupError) {
|
|
2780
|
-
console.error(`[emdash] Failed to clean up unstaged revision ${revision.id}:`, cleanupError);
|
|
2781
|
-
}
|
|
2782
|
-
if (body._rev || attempt === MAX_DRAFT_STAGE_ATTEMPTS - 1) return {
|
|
2783
|
-
success: false,
|
|
2784
|
-
error: {
|
|
2785
|
-
code: "CONFLICT",
|
|
2786
|
-
message: new ContentMutationConflictError().message
|
|
2787
|
-
}
|
|
2788
|
-
};
|
|
2789
|
-
existing = await repo.findById(collection, resolvedId);
|
|
2790
|
-
continue;
|
|
2791
|
-
}
|
|
2792
|
-
draftStorageChanged = true;
|
|
2793
|
-
processedData = attemptData;
|
|
2794
|
-
if (bodyWithoutRev.skipRevision && existing.draftRevisionId) try {
|
|
2795
|
-
await revisionRepo.deleteIfUnreferenced(collection, resolvedId, existing.draftRevisionId);
|
|
2796
|
-
} catch (error) {
|
|
2797
|
-
console.error(`[emdash] Failed to clean up superseded revision ${existing.draftRevisionId}:`, error);
|
|
2798
|
-
}
|
|
2799
|
-
else after(async () => {
|
|
2800
|
-
try {
|
|
2801
|
-
await revisionRepo.pruneQueuedEntry(collection, resolvedId, revision.id, 50);
|
|
2802
|
-
} catch (error) {
|
|
2803
|
-
console.error(`[revisions] Failed to prune revisions for ${collection}/${resolvedId}:`, error);
|
|
2804
|
-
}
|
|
2805
|
-
});
|
|
2806
|
-
break;
|
|
2807
|
-
}
|
|
2808
|
-
}
|
|
2809
|
-
}
|
|
2810
|
-
const liveMetaTouched = Object.entries(bodyWithoutRev).some(([key, value]) => value !== void 0 && !DRAFT_ONLY_UPDATE_KEYS.has(key));
|
|
2811
|
-
const result = usesDraftRevisions && !liveMetaTouched ? await handleContentGet(this.db, collection, resolvedId) : await handleContentUpdate(this.db, collection, resolvedId, {
|
|
2812
|
-
...bodyWithoutRev,
|
|
2813
|
-
data: usesDraftRevisions ? void 0 : processedData,
|
|
2814
|
-
slug: usesDraftRevisions ? void 0 : bodyWithoutRev.slug,
|
|
2815
|
-
authorId: bodyWithoutRev.authorId,
|
|
2816
|
-
bylines: bodyWithoutRev.bylines
|
|
2817
|
-
});
|
|
2818
|
-
const liveContentChanged = usesDraftRevisions ? liveMetaTouched : Boolean(processedData || bodyWithoutRev.slug !== void 0 || liveMetaTouched);
|
|
2819
|
-
const hydrated = await this.hydrateDraftData(result);
|
|
2820
|
-
if (hydrated.success && hydrated.data) {
|
|
2821
|
-
const contentIdsToRefresh = [resolvedId];
|
|
2822
|
-
if (!usesDraftRevisions && processedData) contentIdsToRefresh.push(...await this.findSyncedSiblingsForUsageRefresh(collection, resolvedId, hydrated.data.item.translationGroup, processedData));
|
|
2823
|
-
await this.refreshContentUsageAfterSuccessfulWrite(collection, contentIdsToRefresh);
|
|
2824
|
-
} else if (draftStorageChanged) try {
|
|
2825
|
-
await markContentMediaUsageCollectionStale(this.db, collection, "CONTENT_USAGE_STALE");
|
|
2826
|
-
} catch (error) {
|
|
2827
|
-
console.error(`[media-usage] Failed to mark ${collection} stale:`, error);
|
|
2828
|
-
}
|
|
2829
|
-
if (hydrated.success && hydrated.data) this.runAfterSaveHooks(contentItemToRecord(hydrated.data.item), collection, false, actor);
|
|
2830
|
-
if (hydrated.success) return {
|
|
2831
|
-
...hydrated,
|
|
2832
|
-
liveContentChanged
|
|
2833
|
-
};
|
|
2834
|
-
return hydrated;
|
|
2835
|
-
}
|
|
2836
|
-
async handleContentDelete(collection, id) {
|
|
2837
|
-
if (this.hooks.hasHooks("content:beforeDelete")) {
|
|
2838
|
-
const { allowed } = await this.hooks.runContentBeforeDelete(id, collection);
|
|
2839
|
-
if (!allowed) return {
|
|
2840
|
-
success: false,
|
|
2841
|
-
error: {
|
|
2842
|
-
code: "DELETE_BLOCKED",
|
|
2843
|
-
message: "Delete blocked by plugin hook"
|
|
2844
|
-
}
|
|
2845
|
-
};
|
|
2846
|
-
}
|
|
2847
|
-
const policyRejectionRevision = await this.getScheduledPolicyRejectionRevision(collection, id);
|
|
2848
|
-
const result = await handleContentDelete(this.db, collection, id);
|
|
2849
|
-
if (result.success) {
|
|
2850
|
-
await this.clearScheduledPolicyRejection(collection, result.data.id, policyRejectionRevision);
|
|
2851
|
-
await this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.id]);
|
|
2852
|
-
}
|
|
2853
|
-
if (result.success) this.runAfterDeleteHooks(id, collection, false);
|
|
2854
|
-
return result;
|
|
2855
|
-
}
|
|
2856
|
-
async handleContentListTrashed(collection, params = {}) {
|
|
2857
|
-
return handleContentListTrashed(this.db, collection, params);
|
|
2858
|
-
}
|
|
2859
|
-
async handleContentRestore(collection, id, options = {}) {
|
|
2860
|
-
const result = await handleContentRestore(this.db, collection, id, { _rev: options._rev });
|
|
2861
|
-
if (result.success && result.data) await this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.item.id]);
|
|
2862
|
-
if (result.success) this.runAfterRestoreHooks(contentItemToRecord(result.data.item), collection, options.origin?.source === "plugin" ? options.origin.pluginId : void 0, options.pluginInvocationId);
|
|
2863
|
-
return result;
|
|
2864
|
-
}
|
|
2865
|
-
async handleContentPermanentDelete(collection, id) {
|
|
2866
|
-
const policyRejectionRevision = await this.getScheduledPolicyRejectionRevision(collection, id);
|
|
2867
|
-
const result = await handleContentPermanentDelete(this.db, collection, id);
|
|
2868
|
-
if (result.success) {
|
|
2869
|
-
await this.clearScheduledPolicyRejection(collection, result.data.id, policyRejectionRevision);
|
|
2870
|
-
await this.deleteContentUsageAfterSuccessfulPermanentDelete(collection, result.data.id);
|
|
2871
|
-
}
|
|
2872
|
-
if (result.success) this.runAfterDeleteHooks(id, collection, true);
|
|
2873
|
-
return result;
|
|
2874
|
-
}
|
|
2875
|
-
async handleContentCountTrashed(collection, params = {}) {
|
|
2876
|
-
return handleContentCountTrashed(this.db, collection, params);
|
|
2877
|
-
}
|
|
2878
|
-
async handleContentDuplicate(collection, id, authorId) {
|
|
2879
|
-
const result = await handleContentDuplicate(this.db, collection, id, authorId);
|
|
2880
|
-
if (result.success && result.data) await this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.item.id]);
|
|
2881
|
-
return result;
|
|
2882
|
-
}
|
|
2883
|
-
pluginVersionedResult(result) {
|
|
2884
|
-
if (!result.success) throw Object.assign(new Error(result.error.message), { code: result.error.code });
|
|
2885
|
-
if (!result.data._rev) throw new Error("Content action returned no revision");
|
|
2886
|
-
return {
|
|
2887
|
-
item: toPluginContentItem(result.data.item),
|
|
2888
|
-
_rev: result.data._rev
|
|
2889
|
-
};
|
|
2890
|
-
}
|
|
2891
|
-
async runPluginContentAction(pluginId, action, collection, id, fn, invocationId, invalidateContentCache) {
|
|
2892
|
-
let invocationInvalidator;
|
|
2893
|
-
if (invocationId) {
|
|
2894
|
-
const invocationKey = this.pluginInvocationKey(pluginId, invocationId);
|
|
2895
|
-
if (!this.activePluginInvocations.has(invocationKey) && !this.releasedPluginInvocations.has(invocationKey)) throw Object.assign(/* @__PURE__ */ new Error("Plugin content action invocation has expired"), { code: "CONTENT_ACTION_INVOCATION_EXPIRED" });
|
|
2896
|
-
invocationInvalidator = this.pluginInvocationCacheInvalidators.get(invocationKey);
|
|
2897
|
-
}
|
|
2898
|
-
const recordWrite = await assertSiteWriteAllowed(this.db);
|
|
2899
|
-
const repo = new ContentRepository(this.db);
|
|
2900
|
-
const resolvedId = (action === "restore" ? await repo.findByIdOrSlugIncludingTrashed(collection, id) : await repo.findByIdOrSlug(collection, id))?.id ?? id;
|
|
2901
|
-
const key = this.pluginContentActionKey(collection, resolvedId);
|
|
2902
|
-
if ((this.activePluginContentActions.get(key) ?? 0) > 0) throw Object.assign(/* @__PURE__ */ new Error("Plugin content action re-entered itself"), { code: "CONTENT_ACTION_REENTRANT" });
|
|
2903
|
-
this.retainPluginContentAction(key);
|
|
2904
|
-
try {
|
|
2905
|
-
const result = await fn(resolvedId);
|
|
2906
|
-
await recordWrite();
|
|
2907
|
-
const invalidator = invalidateContentCache ?? invocationInvalidator ?? this.pluginContentCacheInvalidator;
|
|
2908
|
-
if (invalidator) try {
|
|
2909
|
-
await invalidator([collection, resolvedId]);
|
|
2910
|
-
} catch (error) {
|
|
2911
|
-
console.error(`[plugin-content-action] Cache invalidation failed for ${collection}/${resolvedId}:`, error);
|
|
2912
|
-
}
|
|
2913
|
-
return result;
|
|
2914
|
-
} finally {
|
|
2915
|
-
this.releasePluginContentAction(key);
|
|
2916
|
-
}
|
|
2917
|
-
}
|
|
2918
|
-
pluginContentActionKey(collection, id) {
|
|
2919
|
-
return JSON.stringify([collection, id]);
|
|
2920
|
-
}
|
|
2921
|
-
retainPluginContentAction(key) {
|
|
2922
|
-
this.activePluginContentActions.set(key, (this.activePluginContentActions.get(key) ?? 0) + 1);
|
|
2923
|
-
}
|
|
2924
|
-
releasePluginContentAction(key) {
|
|
2925
|
-
const remaining = (this.activePluginContentActions.get(key) ?? 1) - 1;
|
|
2926
|
-
if (remaining > 0) this.activePluginContentActions.set(key, remaining);
|
|
2927
|
-
else this.activePluginContentActions.delete(key);
|
|
2928
|
-
}
|
|
2929
|
-
async pluginGetVersioned(_pluginId, collection, id) {
|
|
2930
|
-
const result = await this.handleContentGet(collection, id);
|
|
2931
|
-
if (!result.success) {
|
|
2932
|
-
if (result.error.code === "NOT_FOUND") return null;
|
|
2933
|
-
throw Object.assign(new Error(result.error.message), { code: result.error.code });
|
|
2934
|
-
}
|
|
2935
|
-
if (!result.data._rev) throw new Error("Content read returned no revision");
|
|
2936
|
-
return {
|
|
2937
|
-
item: toPluginContentItem(result.data.item),
|
|
2938
|
-
_rev: result.data._rev
|
|
2939
|
-
};
|
|
2940
|
-
}
|
|
2941
|
-
async pluginGetTrashedVersioned(_pluginId, collection, id) {
|
|
2942
|
-
const result = await this.handleContentGetIncludingTrashed(collection, id);
|
|
2943
|
-
if (!result.success) {
|
|
2944
|
-
if (result.error.code === "NOT_FOUND") return null;
|
|
2945
|
-
throw Object.assign(new Error(result.error.message), { code: result.error.code });
|
|
2946
|
-
}
|
|
2947
|
-
if (!await new ContentRepository(this.db).isTrashed(collection, result.data.item.id)) return null;
|
|
2948
|
-
if (!result.data._rev) throw new Error("Trashed content read returned no revision");
|
|
2949
|
-
return {
|
|
2950
|
-
item: toPluginContentItem(result.data.item),
|
|
2951
|
-
_rev: result.data._rev
|
|
2952
|
-
};
|
|
2953
|
-
}
|
|
2954
|
-
pluginPublish(pluginId, collection, id, options, invocationId, invalidateContentCache) {
|
|
2955
|
-
return this.runPluginContentAction(pluginId, "publish", collection, id, async (resolvedId) => this.pluginVersionedResult(await this.handleContentPublish(collection, resolvedId, {
|
|
2956
|
-
_rev: options._rev,
|
|
2957
|
-
origin: {
|
|
2958
|
-
source: "plugin",
|
|
2959
|
-
pluginId
|
|
2960
|
-
},
|
|
2961
|
-
pluginInvocationId: invocationId
|
|
2962
|
-
})), invocationId, invalidateContentCache);
|
|
2963
|
-
}
|
|
2964
|
-
pluginUnpublish(pluginId, collection, id, options, invocationId, invalidateContentCache) {
|
|
2965
|
-
return this.runPluginContentAction(pluginId, "unpublish", collection, id, async (resolvedId) => this.pluginVersionedResult(await this.handleContentUnpublish(collection, resolvedId, {
|
|
2966
|
-
_rev: options._rev,
|
|
2967
|
-
origin: {
|
|
2968
|
-
source: "plugin",
|
|
2969
|
-
pluginId
|
|
2970
|
-
},
|
|
2971
|
-
pluginInvocationId: invocationId
|
|
2972
|
-
})), invocationId, invalidateContentCache);
|
|
2973
|
-
}
|
|
2974
|
-
pluginSchedule(pluginId, collection, id, options, invocationId, invalidateContentCache) {
|
|
2975
|
-
return this.runPluginContentAction(pluginId, "schedule", collection, id, async (resolvedId) => this.pluginVersionedResult(await this.handleContentSchedule(collection, resolvedId, options.scheduledAt, {
|
|
2976
|
-
_rev: options._rev,
|
|
2977
|
-
origin: {
|
|
2978
|
-
source: "plugin",
|
|
2979
|
-
pluginId
|
|
2980
|
-
},
|
|
2981
|
-
pluginInvocationId: invocationId
|
|
2982
|
-
})), invocationId, invalidateContentCache);
|
|
2983
|
-
}
|
|
2984
|
-
pluginUnschedule(pluginId, collection, id, options, invocationId, invalidateContentCache) {
|
|
2985
|
-
return this.runPluginContentAction(pluginId, "unschedule", collection, id, async (resolvedId) => this.pluginVersionedResult(await this.handleContentUnschedule(collection, resolvedId, {
|
|
2986
|
-
...options,
|
|
2987
|
-
origin: {
|
|
2988
|
-
source: "plugin",
|
|
2989
|
-
pluginId
|
|
2990
|
-
},
|
|
2991
|
-
pluginInvocationId: invocationId
|
|
2992
|
-
})), invocationId, invalidateContentCache);
|
|
2993
|
-
}
|
|
2994
|
-
pluginRestore(pluginId, collection, id, options, invocationId, invalidateContentCache) {
|
|
2995
|
-
return this.runPluginContentAction(pluginId, "restore", collection, id, async (resolvedId) => this.pluginVersionedResult(await this.handleContentRestore(collection, resolvedId, {
|
|
2996
|
-
...options,
|
|
2997
|
-
origin: {
|
|
2998
|
-
source: "plugin",
|
|
2999
|
-
pluginId
|
|
3000
|
-
},
|
|
3001
|
-
pluginInvocationId: invocationId
|
|
3002
|
-
})), invocationId, invalidateContentCache);
|
|
3003
|
-
}
|
|
3004
|
-
async checkContentPolicy(name, collection, id, options, rejectionCode, failureCode, scheduledAt) {
|
|
3005
|
-
let current = await handleContentGet(this.db, collection, id);
|
|
3006
|
-
if (!current.success) return {
|
|
3007
|
-
allowed: false,
|
|
3008
|
-
revision: options._rev ?? "",
|
|
3009
|
-
error: current.error
|
|
3010
|
-
};
|
|
3011
|
-
const revision = current.data._rev;
|
|
3012
|
-
if (!revision) return {
|
|
3013
|
-
allowed: false,
|
|
3014
|
-
revision: options._rev ?? "",
|
|
3015
|
-
error: {
|
|
3016
|
-
code: failureCode,
|
|
3017
|
-
message: "Failed to read content revision"
|
|
3018
|
-
}
|
|
3019
|
-
};
|
|
3020
|
-
if (options._rev !== void 0 && options._rev !== revision) return {
|
|
3021
|
-
allowed: false,
|
|
3022
|
-
revision,
|
|
3023
|
-
error: {
|
|
3024
|
-
code: "CONFLICT",
|
|
3025
|
-
message: "Revision precondition did not match"
|
|
3026
|
-
}
|
|
3027
|
-
};
|
|
3028
|
-
if (!this.hooks.hasHooks(name)) return {
|
|
3029
|
-
allowed: true,
|
|
3030
|
-
revision
|
|
3031
|
-
};
|
|
3032
|
-
if (name !== "content:beforeUnpublish") try {
|
|
3033
|
-
current = await this.hydrateDraftData(current, {
|
|
3034
|
-
includeStagedSlug: true,
|
|
3035
|
-
strict: true
|
|
3036
|
-
});
|
|
3037
|
-
} catch (error) {
|
|
3038
|
-
console.error(`[content-policy] ${name} draft hydration failed:`, error);
|
|
3039
|
-
return {
|
|
3040
|
-
allowed: false,
|
|
3041
|
-
revision,
|
|
3042
|
-
error: {
|
|
3043
|
-
code: failureCode,
|
|
3044
|
-
message: `Failed to read content for ${name} policy`
|
|
3045
|
-
}
|
|
3046
|
-
};
|
|
3047
|
-
}
|
|
3048
|
-
const origin = options.origin ?? { source: "system" };
|
|
3049
|
-
const actor = options.actor && (origin.source === "api" || origin.source === "mcp" || origin.source === "visual-editor") ? {
|
|
3050
|
-
...options.actor,
|
|
3051
|
-
source: origin.source
|
|
3052
|
-
} : void 0;
|
|
3053
|
-
try {
|
|
3054
|
-
const decision = await this.hooks.runContentPolicy(name, {
|
|
3055
|
-
content: contentItemToRecord(current.data.item),
|
|
3056
|
-
collection,
|
|
3057
|
-
origin,
|
|
3058
|
-
actor,
|
|
3059
|
-
...scheduledAt === void 0 ? {} : { scheduledAt }
|
|
3060
|
-
});
|
|
3061
|
-
if (decision.cancellation) return {
|
|
3062
|
-
allowed: false,
|
|
3063
|
-
revision,
|
|
3064
|
-
error: {
|
|
3065
|
-
code: rejectionCode,
|
|
3066
|
-
message: decision.cancellation.reason
|
|
3067
|
-
},
|
|
3068
|
-
cancellation: decision.cancellation
|
|
3069
|
-
};
|
|
3070
|
-
return {
|
|
3071
|
-
allowed: true,
|
|
3072
|
-
revision
|
|
3073
|
-
};
|
|
3074
|
-
} catch (error) {
|
|
3075
|
-
console.error(`[content-policy] ${name} failed:`, error);
|
|
3076
|
-
return {
|
|
3077
|
-
allowed: false,
|
|
3078
|
-
revision,
|
|
3079
|
-
error: {
|
|
3080
|
-
code: failureCode,
|
|
3081
|
-
message: `Failed to run ${name} policy`
|
|
3082
|
-
}
|
|
3083
|
-
};
|
|
3084
|
-
}
|
|
3085
|
-
}
|
|
3086
|
-
async getScheduledPolicyRejectionRevision(collection, id) {
|
|
3087
|
-
try {
|
|
3088
|
-
return (await new OptionsRepository(this.db).getVersioned(scheduledPolicyRejectionKey(collection, id)))?.revision;
|
|
3089
|
-
} catch (error) {
|
|
3090
|
-
console.error(`[content-policy] Failed to read scheduled rejection for ${collection}/${id}:`, error);
|
|
3091
|
-
return;
|
|
3092
|
-
}
|
|
3093
|
-
}
|
|
3094
|
-
async clearScheduledPolicyRejection(collection, id, expectedRevision) {
|
|
3095
|
-
if (expectedRevision === void 0) return;
|
|
3096
|
-
try {
|
|
3097
|
-
await new OptionsRepository(this.db).compareAndDelete(scheduledPolicyRejectionKey(collection, id), expectedRevision);
|
|
3098
|
-
} catch (error) {
|
|
3099
|
-
console.error(`[content-policy] Failed to clear scheduled rejection for ${collection}/${id}:`, error);
|
|
3100
|
-
}
|
|
3101
|
-
}
|
|
3102
|
-
createScheduledPolicyRejection(collection, id, cancellation) {
|
|
3103
|
-
return {
|
|
3104
|
-
collection,
|
|
3105
|
-
id,
|
|
3106
|
-
pluginId: cancellation.pluginId,
|
|
3107
|
-
reason: cancellation.reason,
|
|
3108
|
-
rejectedAt: (this.runtimeDeps.now?.() ?? /* @__PURE__ */ new Date()).toISOString()
|
|
3109
|
-
};
|
|
3110
|
-
}
|
|
3111
|
-
async handleContentPublish(collection, id, options = {}) {
|
|
3112
|
-
const policy = await this.checkContentPolicy("content:beforePublish", collection, id, options, "PUBLISH_REJECTED", "CONTENT_PUBLISH_ERROR");
|
|
3113
|
-
if (!policy.allowed) {
|
|
3114
|
-
if (policy.cancellation && options.origin?.source === "scheduler") {
|
|
3115
|
-
const unscheduled = await handleScheduledPolicyRejection(this.db, collection, id, {
|
|
3116
|
-
_rev: policy.revision,
|
|
3117
|
-
rejection: this.createScheduledPolicyRejection(collection, id, policy.cancellation)
|
|
3118
|
-
});
|
|
3119
|
-
if (!unscheduled.success) return unscheduled;
|
|
3120
|
-
await this.refreshContentUsageAfterSuccessfulWrite(collection, [unscheduled.data.item.id]);
|
|
3121
|
-
this.runAfterUnscheduleHooks(contentItemToRecord(unscheduled.data.item), collection);
|
|
3122
|
-
}
|
|
3123
|
-
return {
|
|
3124
|
-
success: false,
|
|
3125
|
-
error: policy.error
|
|
3126
|
-
};
|
|
3127
|
-
}
|
|
3128
|
-
const policyRejectionRevision = await this.getScheduledPolicyRejectionRevision(collection, id);
|
|
3129
|
-
const { actor: _actor, origin: _origin, pluginInvocationId: _pluginInvocationId, ...handlerOptions } = options;
|
|
3130
|
-
const result = await handleContentPublish(this.db, collection, id, {
|
|
3131
|
-
...handlerOptions,
|
|
3132
|
-
_rev: policy.revision
|
|
3133
|
-
});
|
|
3134
|
-
if (result.success && result.data) {
|
|
3135
|
-
await this.clearScheduledPolicyRejection(collection, result.data.item.id, policyRejectionRevision);
|
|
3136
|
-
const { item } = result.data;
|
|
3137
|
-
await this.refreshContentUsageAfterSuccessfulWrite(collection, [item.id, ...await this.findSyncedSiblingsForUsageRefresh(collection, item.id, item.translationGroup, item.data, { absentAsCleared: true })]);
|
|
3138
|
-
}
|
|
3139
|
-
if (result.success && result.data) this.runAfterPublishHooks(contentItemToRecord(result.data.item), collection, options.origin?.source === "plugin" ? options.origin.pluginId : void 0, options.pluginInvocationId);
|
|
3140
|
-
return result;
|
|
3141
|
-
}
|
|
3142
|
-
async handleContentUnpublish(collection, id, options = {}) {
|
|
3143
|
-
const policy = await this.checkContentPolicy("content:beforeUnpublish", collection, id, options, "UNPUBLISH_REJECTED", "CONTENT_UNPUBLISH_ERROR");
|
|
3144
|
-
if (!policy.allowed) return {
|
|
3145
|
-
success: false,
|
|
3146
|
-
error: policy.error
|
|
3147
|
-
};
|
|
3148
|
-
const result = await handleContentUnpublish(this.db, collection, id, { _rev: policy.revision });
|
|
3149
|
-
if (result.success && result.data) await this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.item.id]);
|
|
3150
|
-
if (result.success && result.data) this.runAfterUnpublishHooks(contentItemToRecord(result.data.item), collection, options.origin?.source === "plugin" ? options.origin.pluginId : void 0, options.pluginInvocationId);
|
|
3151
|
-
return result;
|
|
3152
|
-
}
|
|
3153
|
-
async handleContentSchedule(collection, id, scheduledAt, options = {}) {
|
|
3154
|
-
const policy = await this.checkContentPolicy("content:beforeSchedule", collection, id, options, "SCHEDULE_REJECTED", "CONTENT_SCHEDULE_ERROR", scheduledAt);
|
|
3155
|
-
if (!policy.allowed) return {
|
|
3156
|
-
success: false,
|
|
3157
|
-
error: policy.error
|
|
3158
|
-
};
|
|
3159
|
-
const policyRejectionRevision = await this.getScheduledPolicyRejectionRevision(collection, id);
|
|
3160
|
-
const result = await handleContentSchedule(this.db, collection, id, scheduledAt, this.runtimeDeps.now?.() ?? /* @__PURE__ */ new Date(), policy.revision);
|
|
3161
|
-
if (result.success && result.data) {
|
|
3162
|
-
await this.clearScheduledPolicyRejection(collection, result.data.item.id, policyRejectionRevision);
|
|
3163
|
-
await this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.item.id]);
|
|
3164
|
-
}
|
|
3165
|
-
if (result.success && result.data) this.runAfterScheduleHooks(contentItemToRecord(result.data.item), collection, options.origin?.source === "plugin" ? options.origin.pluginId : void 0, options.pluginInvocationId);
|
|
3166
|
-
return result;
|
|
3167
|
-
}
|
|
3168
|
-
async handleContentUnschedule(collection, id, options = {}) {
|
|
3169
|
-
const result = await handleContentUnschedule(this.db, collection, id, { _rev: options._rev });
|
|
3170
|
-
if (result.success && result.data) await this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.item.id]);
|
|
3171
|
-
if (result.success && result.data) this.runAfterUnscheduleHooks(contentItemToRecord(result.data.item), collection, options.origin?.source === "plugin" ? options.origin.pluginId : void 0, options.pluginInvocationId);
|
|
3172
|
-
return result;
|
|
3173
|
-
}
|
|
3174
|
-
async handleContentCountScheduled(collection) {
|
|
3175
|
-
return handleContentCountScheduled(this.db, collection);
|
|
3176
|
-
}
|
|
3177
|
-
async handleContentDiscardDraft(collection, id, options = {}) {
|
|
3178
|
-
const result = await handleContentDiscardDraft(this.db, collection, id, options);
|
|
3179
|
-
if (result.success && result.data) await this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.item.id]);
|
|
3180
|
-
return result;
|
|
3181
|
-
}
|
|
3182
|
-
async handleContentCompare(collection, id) {
|
|
3183
|
-
return handleContentCompare(this.db, collection, id);
|
|
3184
|
-
}
|
|
3185
|
-
async handleContentTranslations(collection, id) {
|
|
3186
|
-
return handleContentTranslations(this.db, collection, id);
|
|
3187
|
-
}
|
|
3188
|
-
async handleMediaList(params) {
|
|
3189
|
-
return handleMediaList(this.db, params);
|
|
3190
|
-
}
|
|
3191
|
-
async handleMediaGet(id) {
|
|
3192
|
-
return handleMediaGet(this.db, id);
|
|
3193
|
-
}
|
|
3194
|
-
async handleMediaUpload(input) {
|
|
3195
|
-
if (!this.storage) return {
|
|
3196
|
-
success: false,
|
|
3197
|
-
error: {
|
|
3198
|
-
code: "NO_STORAGE",
|
|
3199
|
-
message: "Storage not configured"
|
|
3200
|
-
}
|
|
3201
|
-
};
|
|
3202
|
-
const result = await handleMediaUpload(this.db, this.storage, input, { beforeUpload: async (file) => {
|
|
3203
|
-
if (!this.hooks.hasHooks("media:beforeUpload")) return file;
|
|
3204
|
-
return (await this.hooks.runMediaBeforeUpload(file)).file;
|
|
3205
|
-
} });
|
|
3206
|
-
if (result.success && !result.data.deduplicated && this.hooks.hasHooks("media:afterUpload")) {
|
|
3207
|
-
const item = result.data.item;
|
|
3208
|
-
await this.hooks.runMediaAfterUpload({
|
|
3209
|
-
id: item.id,
|
|
3210
|
-
filename: item.filename,
|
|
3211
|
-
mimeType: item.mimeType,
|
|
3212
|
-
size: item.size,
|
|
3213
|
-
url: item.url,
|
|
3214
|
-
createdAt: item.createdAt
|
|
3215
|
-
});
|
|
3216
|
-
}
|
|
3217
|
-
return result;
|
|
3218
|
-
}
|
|
3219
|
-
async handleMediaCreate(input) {
|
|
3220
|
-
let processedInput = input;
|
|
3221
|
-
if (this.hooks.hasHooks("media:beforeUpload")) {
|
|
3222
|
-
const hookResult = await this.hooks.runMediaBeforeUpload({
|
|
3223
|
-
name: input.filename,
|
|
3224
|
-
type: input.mimeType,
|
|
3225
|
-
size: input.size || 0
|
|
3226
|
-
});
|
|
3227
|
-
processedInput = {
|
|
3228
|
-
...input,
|
|
3229
|
-
filename: hookResult.file.name,
|
|
3230
|
-
mimeType: hookResult.file.type,
|
|
3231
|
-
size: hookResult.file.size
|
|
3232
|
-
};
|
|
3233
|
-
}
|
|
3234
|
-
const result = await handleMediaCreate(this.db, processedInput);
|
|
3235
|
-
if (result.success && this.hooks.hasHooks("media:afterUpload")) {
|
|
3236
|
-
const item = result.data.item;
|
|
3237
|
-
const mediaItem = {
|
|
3238
|
-
id: item.id,
|
|
3239
|
-
filename: item.filename,
|
|
3240
|
-
mimeType: item.mimeType,
|
|
3241
|
-
size: item.size,
|
|
3242
|
-
url: `/media/${item.id}/${item.filename}`,
|
|
3243
|
-
createdAt: item.createdAt
|
|
3244
|
-
};
|
|
3245
|
-
this.hooks.runMediaAfterUpload(mediaItem).catch((err) => console.error("EmDash afterUpload hook error:", err));
|
|
3246
|
-
}
|
|
3247
|
-
return result;
|
|
3248
|
-
}
|
|
3249
|
-
async handleMediaUpdate(id, input) {
|
|
3250
|
-
const result = await handleMediaUpdate(this.db, id, input);
|
|
3251
|
-
if (result.success) invalidateSiteSettingsCache();
|
|
3252
|
-
return result;
|
|
3253
|
-
}
|
|
3254
|
-
async handlePluginMediaMetadataUpdate(id, patch) {
|
|
3255
|
-
return updatePluginMediaMetadata(this.db, id, patch);
|
|
3256
|
-
}
|
|
3257
|
-
async handleMediaReplaceMetadata(id, expectedStorageKey, input) {
|
|
3258
|
-
const result = await handleMediaReplaceMetadata(this.db, id, expectedStorageKey, input);
|
|
3259
|
-
if (result.success) invalidateSiteSettingsCache();
|
|
3260
|
-
return result;
|
|
3261
|
-
}
|
|
3262
|
-
async handleMediaDelete(id) {
|
|
3263
|
-
const result = await handleMediaDelete(this.db, id, this.storage);
|
|
3264
|
-
if (result.success) invalidateSiteSettingsCache();
|
|
3265
|
-
return result;
|
|
3266
|
-
}
|
|
3267
|
-
commentHooks() {
|
|
3268
|
-
return {
|
|
3269
|
-
runBeforeCreate: (event) => this.hooks.runCommentBeforeCreate(event),
|
|
3270
|
-
runModerate: async (event) => {
|
|
3271
|
-
const result = await this.hooks.invokeExclusiveHook("comment:moderate", event);
|
|
3272
|
-
if (!result) return {
|
|
3273
|
-
status: "pending",
|
|
3274
|
-
reason: "No moderator configured"
|
|
3275
|
-
};
|
|
3276
|
-
if (result.error) {
|
|
3277
|
-
console.error(`[comments] Moderation error (${result.pluginId}):`, result.error.message);
|
|
3278
|
-
return {
|
|
3279
|
-
status: "pending",
|
|
3280
|
-
reason: "Moderation error"
|
|
3281
|
-
};
|
|
3282
|
-
}
|
|
3283
|
-
if (!isModerationDecision(result.result)) return {
|
|
3284
|
-
status: "pending",
|
|
3285
|
-
reason: "Invalid moderation result"
|
|
3286
|
-
};
|
|
3287
|
-
return result.result;
|
|
3288
|
-
},
|
|
3289
|
-
fireAfterCreate: (event) => {
|
|
3290
|
-
this.hooks.runCommentAfterCreate(event).catch((error) => console.error("[comments] afterCreate error:", error instanceof Error ? error.message : error));
|
|
3291
|
-
},
|
|
3292
|
-
fireAfterModerate: (event) => {
|
|
3293
|
-
return this.hooks.runCommentAfterModerate(event).catch((error) => console.error("[comments] afterModerate error:", error instanceof Error ? error.message : error));
|
|
3294
|
-
}
|
|
3295
|
-
};
|
|
3296
|
-
}
|
|
3297
|
-
async handleCommentCreate(input, settings, contentInfo) {
|
|
3298
|
-
return createComment(this.db, input, settings, this.commentHooks(), contentInfo);
|
|
3299
|
-
}
|
|
3300
|
-
async handlePublicCommentSubmission(collection, contentId, request, user) {
|
|
3301
|
-
return submitPublicComment(this, collection, contentId, request, user);
|
|
3302
|
-
}
|
|
3303
|
-
async sendCommentApproval(comment, request) {
|
|
3304
|
-
if (!comment || !this.email) return;
|
|
3305
|
-
try {
|
|
3306
|
-
const adminBaseUrl = await getSiteBaseUrl(this.db, request ?? new Request("https://plugin.invalid/"), this.config);
|
|
3307
|
-
const content = await lookupContentAuthor(this.db, comment.collection, comment.contentId);
|
|
3308
|
-
if (content?.author) await sendCommentNotification({
|
|
3309
|
-
email: this.email,
|
|
3310
|
-
comment,
|
|
3311
|
-
contentAuthor: content.author,
|
|
3312
|
-
adminBaseUrl
|
|
3313
|
-
});
|
|
3314
|
-
} catch (error) {
|
|
3315
|
-
console.error("[comments] notification error:", error instanceof Error ? error.message : error);
|
|
3316
|
-
}
|
|
3317
|
-
}
|
|
3318
|
-
async moderateCommentWithOrigin(id, status, expectedStatus, origin, request) {
|
|
3319
|
-
if (this.commentModerationInProgress.has(id)) {
|
|
3320
|
-
const current = await new CommentRepository(this.db).findById(id);
|
|
3321
|
-
if (current && current.status !== expectedStatus) throw new CommentStatusConflictError(current.status);
|
|
3322
|
-
throw Object.assign(/* @__PURE__ */ new Error("Comment moderation is already in progress"), { code: "COMMENT_MODERATION_IN_PROGRESS" });
|
|
3323
|
-
}
|
|
3324
|
-
this.commentModerationInProgress.add(id);
|
|
3325
|
-
try {
|
|
3326
|
-
return await moderateComment(this.db, id, status, expectedStatus, origin, this.commentHooks(), (comment) => this.sendCommentApproval(comment, request));
|
|
3327
|
-
} finally {
|
|
3328
|
-
this.commentModerationInProgress.delete(id);
|
|
3329
|
-
}
|
|
3330
|
-
}
|
|
3331
|
-
async handleCommentModerate(id, status, moderator, expectedStatus, request) {
|
|
3332
|
-
let expected = expectedStatus;
|
|
3333
|
-
if (!expected) {
|
|
3334
|
-
const current = await new CommentRepository(this.db).findById(id);
|
|
3335
|
-
if (!current || current.status === "trash") return null;
|
|
3336
|
-
expected = current.status;
|
|
3337
|
-
}
|
|
3338
|
-
return this.moderateCommentWithOrigin(id, status, expected, {
|
|
3339
|
-
source: "admin",
|
|
3340
|
-
userId: moderator.id,
|
|
3341
|
-
name: moderator.name
|
|
3342
|
-
}, request);
|
|
3343
|
-
}
|
|
3344
|
-
async handlePluginCommentModerate(pluginId, id, status, expectedStatus) {
|
|
3345
|
-
assertPluginCommentStatus(status, "status");
|
|
3346
|
-
assertPluginCommentStatus(expectedStatus, "expectedStatus");
|
|
3347
|
-
if (!await this.moderateCommentWithOrigin(id, status, expectedStatus, {
|
|
3348
|
-
source: "plugin",
|
|
3349
|
-
pluginId
|
|
3350
|
-
})) throw new Error(`Comment not found: ${id}`);
|
|
3351
|
-
const result = await createCommentAccess(this.db).get(id);
|
|
3352
|
-
if (!result) throw new Error(`Comment not found: ${id}`);
|
|
3353
|
-
return result;
|
|
3354
|
-
}
|
|
3355
|
-
async handleRevisionList(collection, entryId, params = {}) {
|
|
3356
|
-
return handleRevisionList(this.db, collection, entryId, params);
|
|
3357
|
-
}
|
|
3358
|
-
async handleRevisionGet(revisionId) {
|
|
3359
|
-
return handleRevisionGet(this.db, revisionId);
|
|
3360
|
-
}
|
|
3361
|
-
async handleRevisionRestore(revisionId, callerUserId) {
|
|
3362
|
-
const revisionRepo = new RevisionRepository(this.db);
|
|
3363
|
-
const revision = await revisionRepo.findById(revisionId);
|
|
3364
|
-
if (!revision) return {
|
|
3365
|
-
success: false,
|
|
3366
|
-
error: {
|
|
3367
|
-
code: "NOT_FOUND",
|
|
3368
|
-
message: `Revision not found: ${revisionId}`
|
|
3369
|
-
}
|
|
3370
|
-
};
|
|
3371
|
-
if (!((await this.schemaRegistry.getCollectionWithFields(revision.collection))?.supports?.includes("revisions") ?? false)) {
|
|
3372
|
-
const result = await handleRevisionRestore(this.db, revisionId, callerUserId);
|
|
3373
|
-
if (result.success) await this.refreshContentUsageAfterSuccessfulWrite(revision.collection, [revision.entryId]);
|
|
3374
|
-
return this.hydrateDraftData(result);
|
|
3375
|
-
}
|
|
3376
|
-
try {
|
|
3377
|
-
const newDraftId = await new ContentRepository(this.db).restoreDraftRevision(revision.collection, revision.entryId, revision.data, callerUserId);
|
|
3378
|
-
if (!newDraftId) return {
|
|
3379
|
-
success: false,
|
|
3380
|
-
error: {
|
|
3381
|
-
code: "NOT_FOUND",
|
|
3382
|
-
message: `Content item not found: ${revision.entryId}`
|
|
3383
|
-
}
|
|
3384
|
-
};
|
|
3385
|
-
after(async () => {
|
|
3386
|
-
try {
|
|
3387
|
-
await revisionRepo.pruneQueuedEntry(revision.collection, revision.entryId, newDraftId, 50);
|
|
3388
|
-
} catch (error) {
|
|
3389
|
-
console.error(`[revisions] Failed to prune revisions for ${revision.collection}/${revision.entryId}:`, error);
|
|
3390
|
-
}
|
|
3391
|
-
});
|
|
3392
|
-
const refetched = await handleContentGet(this.db, revision.collection, revision.entryId);
|
|
3393
|
-
const hydrated = await this.hydrateDraftData(refetched);
|
|
3394
|
-
if (hydrated.success) await this.refreshContentUsageAfterSuccessfulWrite(revision.collection, [revision.entryId]);
|
|
3395
|
-
return hydrated;
|
|
3396
|
-
} catch (error) {
|
|
3397
|
-
if (error instanceof ContentMutationConflictError) return {
|
|
3398
|
-
success: false,
|
|
3399
|
-
error: {
|
|
3400
|
-
code: "CONFLICT",
|
|
3401
|
-
message: error.message
|
|
3402
|
-
}
|
|
3403
|
-
};
|
|
3404
|
-
console.error("[emdash] revision restore failed:", error);
|
|
3405
|
-
return {
|
|
3406
|
-
success: false,
|
|
3407
|
-
error: {
|
|
3408
|
-
code: "REVISION_RESTORE_ERROR",
|
|
3409
|
-
message: "Failed to restore revision"
|
|
3410
|
-
}
|
|
3411
|
-
};
|
|
3412
|
-
}
|
|
3413
|
-
}
|
|
3414
|
-
async findSyncedSiblingsForUsageRefresh(collection, contentId, translationGroup, data, options = {}) {
|
|
3415
|
-
try {
|
|
3416
|
-
return await findNonTranslatableSiblingContentIds(this.db, collection, contentId, translationGroup, data, options);
|
|
3417
|
-
} catch (error) {
|
|
3418
|
-
console.error(`[media-usage] Failed to discover synced i18n siblings for ${collection}/${contentId}:`, error);
|
|
3419
|
-
try {
|
|
3420
|
-
await markContentMediaUsageCollectionStale(this.db, collection, "CONTENT_USAGE_REFRESH_ERROR");
|
|
3421
|
-
} catch (staleError) {
|
|
3422
|
-
console.error(`[media-usage] Failed to mark ${collection} stale:`, staleError);
|
|
3423
|
-
}
|
|
3424
|
-
return [];
|
|
3425
|
-
}
|
|
3426
|
-
}
|
|
3427
|
-
async refreshContentUsageAfterSuccessfulWrite(collection, contentIds) {
|
|
3428
|
-
for (const contentId of new Set(contentIds)) try {
|
|
3429
|
-
if ((await processMediaUsageWorkAfterWrite(this.db, collection, contentId)).outcome !== "inactive") continue;
|
|
3430
|
-
await refreshContentMediaUsageAfterWrite(this.db, collection, contentId);
|
|
3431
|
-
} catch (error) {
|
|
3432
|
-
console.error(`[media-usage] Failed after content write ${collection}/${contentId}:`, error);
|
|
3433
|
-
continue;
|
|
3434
|
-
}
|
|
3435
|
-
}
|
|
3436
|
-
async deleteContentUsageAfterSuccessfulPermanentDelete(collection, contentId) {
|
|
3437
|
-
try {
|
|
3438
|
-
if ((await processMediaUsageWorkAfterWrite(this.db, collection, contentId)).outcome !== "inactive") return;
|
|
3439
|
-
const result = await deleteContentMediaUsage(this.db, collection, contentId);
|
|
3440
|
-
if (!result.success) console.error(`[media-usage] Usage delete for ${collection}/${contentId} finished with ${result.errorCode}`);
|
|
3441
|
-
} catch (error) {
|
|
3442
|
-
console.error(`[media-usage] Failed after permanent content delete ${collection}/${contentId}:`, error);
|
|
3443
|
-
}
|
|
3444
|
-
}
|
|
3445
|
-
getSandboxedAdminDefinition(pluginId) {
|
|
3446
|
-
const entry = this.sandboxedPluginEntries.find((candidate) => candidate.id === pluginId);
|
|
3447
|
-
if (entry) {
|
|
3448
|
-
const pages = (entry.adminPages ?? []).map((page) => normalizePluginPagePath(page.path));
|
|
3449
|
-
const imageHosts = allowedBrowserImageHosts(entry.capabilities, entry.allowedHosts);
|
|
3450
|
-
return {
|
|
3451
|
-
pages,
|
|
3452
|
-
widgets: (entry.adminWidgets ?? []).map((widget) => widget.id),
|
|
3453
|
-
policy: {
|
|
3454
|
-
pluginPagePaths: pages,
|
|
3455
|
-
allowedImageHosts: imageHosts
|
|
3456
|
-
}
|
|
3457
|
-
};
|
|
3458
|
-
}
|
|
3459
|
-
const manifest = marketplaceManifestCache.get(pluginId);
|
|
3460
|
-
if (!manifest) return null;
|
|
3461
|
-
const pages = (manifest.admin?.pages ?? []).map((page) => normalizePluginPagePath(page.path));
|
|
3462
|
-
const imageHosts = allowedBrowserImageHosts(manifest.capabilities ?? [], manifest.allowedHosts ?? []);
|
|
3463
|
-
return {
|
|
3464
|
-
pages,
|
|
3465
|
-
widgets: (manifest.admin?.widgets ?? []).map((widget) => widget.id),
|
|
3466
|
-
policy: {
|
|
3467
|
-
pluginPagePaths: pages,
|
|
3468
|
-
allowedImageHosts: imageHosts
|
|
3469
|
-
}
|
|
3470
|
-
};
|
|
3471
|
-
}
|
|
3472
|
-
resolvePluginUiContext(definition, body, request) {
|
|
3473
|
-
if (typeof body !== "object" || body === null || !("page" in body)) return {};
|
|
3474
|
-
const page = body.page;
|
|
3475
|
-
if (typeof page !== "string") return { error: {
|
|
3476
|
-
code: "INVALID_PLUGIN_UI_CONTEXT",
|
|
3477
|
-
message: "Plugin UI page must be a string"
|
|
3478
|
-
} };
|
|
3479
|
-
let surface;
|
|
3480
|
-
if (page.startsWith("widget:")) {
|
|
3481
|
-
if (!definition.widgets.includes(page.slice(7))) return { error: {
|
|
3482
|
-
code: "INVALID_PLUGIN_UI_CONTEXT",
|
|
3483
|
-
message: "Plugin dashboard widget is not declared"
|
|
3484
|
-
} };
|
|
3485
|
-
surface = "dashboard-widget";
|
|
3486
|
-
} else {
|
|
3487
|
-
if (!definition.pages.includes(normalizePluginPagePath(page))) return { error: {
|
|
3488
|
-
code: "INVALID_PLUGIN_UI_CONTEXT",
|
|
3489
|
-
message: "Plugin admin page is not declared"
|
|
3490
|
-
} };
|
|
3491
|
-
surface = "admin-page";
|
|
3492
|
-
}
|
|
3493
|
-
const locale = resolveLocale(request);
|
|
3494
|
-
return { context: {
|
|
3495
|
-
surface,
|
|
3496
|
-
locale,
|
|
3497
|
-
direction: getLocaleDir(locale)
|
|
3498
|
-
} };
|
|
3499
|
-
}
|
|
3500
|
-
validateSandboxedAdminResponse(pluginId, definition, result) {
|
|
3501
|
-
if (!result.success) return result;
|
|
3502
|
-
const validation = validateBlockResponse(result.data, definition.policy);
|
|
3503
|
-
if (validation.valid) return result;
|
|
3504
|
-
console.error(`EmDash: Sandboxed plugin ${pluginId} returned invalid Block Kit content:`, validation.errors);
|
|
3505
|
-
return {
|
|
3506
|
-
success: false,
|
|
3507
|
-
status: 502,
|
|
3508
|
-
error: {
|
|
3509
|
-
code: "INVALID_BLOCK_RESPONSE",
|
|
3510
|
-
message: "Plugin returned invalid Block Kit content"
|
|
3511
|
-
}
|
|
3512
|
-
};
|
|
3513
|
-
}
|
|
3514
|
-
getPluginEditorExtension(pluginId, kind, extensionId, collection) {
|
|
3515
|
-
if (!this.isPluginEnabled(pluginId)) return null;
|
|
3516
|
-
let panels;
|
|
3517
|
-
let actions;
|
|
3518
|
-
let pages = [];
|
|
3519
|
-
let capabilities = [];
|
|
3520
|
-
let allowedHosts = [];
|
|
3521
|
-
const configured = this.configuredPlugins.find((plugin) => plugin.id === pluginId);
|
|
3522
|
-
if (configured) {
|
|
3523
|
-
if (configured.admin.entry) return null;
|
|
3524
|
-
panels = configured.admin.editorPanels;
|
|
3525
|
-
actions = configured.admin.editorActions;
|
|
3526
|
-
pages = (configured.admin.pages ?? []).map((page) => page.path);
|
|
3527
|
-
capabilities = configured.capabilities;
|
|
3528
|
-
allowedHosts = configured.allowedHosts;
|
|
3529
|
-
} else {
|
|
3530
|
-
const entry = this.sandboxedPluginEntries.find((candidate) => candidate.id === pluginId);
|
|
3531
|
-
if (entry) {
|
|
3532
|
-
panels = entry.editorPanels;
|
|
3533
|
-
actions = entry.editorActions;
|
|
3534
|
-
pages = (entry.adminPages ?? []).map((page) => page.path);
|
|
3535
|
-
capabilities = entry.capabilities;
|
|
3536
|
-
allowedHosts = entry.allowedHosts;
|
|
3537
|
-
} else {
|
|
3538
|
-
const manifest = marketplaceManifestCache.get(pluginId);
|
|
3539
|
-
if (!manifest) return null;
|
|
3540
|
-
panels = manifest.admin?.editorPanels;
|
|
3541
|
-
actions = manifest.admin?.editorActions;
|
|
3542
|
-
pages = (manifest.admin?.pages ?? []).map((page) => page.path);
|
|
3543
|
-
capabilities = manifest.capabilities ?? [];
|
|
3544
|
-
allowedHosts = manifest.allowedHosts ?? [];
|
|
3545
|
-
}
|
|
3546
|
-
}
|
|
3547
|
-
const normalizedCapabilities = normalizePluginCapabilities(capabilities);
|
|
3548
|
-
const policy = {
|
|
3549
|
-
pluginPagePaths: pages,
|
|
3550
|
-
allowedImageHosts: allowedBrowserImageHosts(capabilities, allowedHosts)
|
|
3551
|
-
};
|
|
3552
|
-
if (kind === "panel") {
|
|
3553
|
-
const matches = panels?.filter((panel) => panel.id === extensionId) ?? [];
|
|
3554
|
-
const extension = matches[0];
|
|
3555
|
-
if (matches.length !== 1 || !extension || extension.collections && !extension.collections.includes(collection)) return null;
|
|
3556
|
-
return {
|
|
3557
|
-
kind,
|
|
3558
|
-
extension,
|
|
3559
|
-
policy,
|
|
3560
|
-
capabilities: normalizedCapabilities
|
|
3561
|
-
};
|
|
3562
|
-
}
|
|
3563
|
-
const matches = actions?.filter((action) => action.id === extensionId) ?? [];
|
|
3564
|
-
const extension = matches[0];
|
|
3565
|
-
if (matches.length !== 1 || !extension || extension.collections && !extension.collections.includes(collection) || extension.style === "danger" && !extension.confirm) return null;
|
|
3566
|
-
return {
|
|
3567
|
-
kind,
|
|
3568
|
-
extension,
|
|
3569
|
-
policy,
|
|
3570
|
-
capabilities: normalizedCapabilities
|
|
3571
|
-
};
|
|
3572
|
-
}
|
|
3573
|
-
async getPluginEditorDraftSchema(collection) {
|
|
3574
|
-
return this.schemaRegistry.getCollectionWithFields(collection);
|
|
3575
|
-
}
|
|
3576
|
-
validatePluginEditorExtensionResponse(pluginId, dispatch, result) {
|
|
3577
|
-
if (!result.success) return result;
|
|
3578
|
-
const validation = dispatch.kind === "panel" ? validateBlockResponse(result.data, dispatch.policy) : validateContentEditorActionResponse(result.data, dispatch.policy);
|
|
3579
|
-
if (validation.valid) return result;
|
|
3580
|
-
console.error(`EmDash: Plugin ${pluginId} returned an invalid content editor ${dispatch.kind} response:`, validation.errors);
|
|
3581
|
-
return {
|
|
3582
|
-
success: false,
|
|
3583
|
-
status: 502,
|
|
3584
|
-
error: {
|
|
3585
|
-
code: dispatch.kind === "panel" ? "INVALID_BLOCK_RESPONSE" : "INVALID_EDITOR_ACTION_RESPONSE",
|
|
3586
|
-
message: "Plugin returned an invalid editor extension response"
|
|
3587
|
-
}
|
|
3588
|
-
};
|
|
3589
|
-
}
|
|
3590
|
-
/**
|
|
3591
|
-
* Get route metadata for a plugin route without invoking the handler.
|
|
3592
|
-
* Used by the catch-all route to decide auth before dispatch.
|
|
3593
|
-
* Returns null if the plugin or route doesn't exist.
|
|
3594
|
-
*/
|
|
3595
|
-
getPluginRouteMeta(pluginId, path) {
|
|
3596
|
-
if (!this.isPluginEnabled(pluginId)) return null;
|
|
3597
|
-
const routeKey = path.replace(LEADING_SLASH_PATTERN, "");
|
|
3598
|
-
const trustedPlugin = this.configuredPlugins.find((p) => p.id === pluginId);
|
|
3599
|
-
if (trustedPlugin) {
|
|
3600
|
-
const route = trustedPlugin.routes[routeKey];
|
|
3601
|
-
if (!route) return null;
|
|
3602
|
-
return buildRouteMeta(route);
|
|
3603
|
-
}
|
|
3604
|
-
const meta = sandboxedRouteMetaCache.get(pluginId);
|
|
3605
|
-
if (meta) {
|
|
3606
|
-
const routeMeta = meta.get(routeKey);
|
|
3607
|
-
if (routeMeta) return routeMeta;
|
|
3608
|
-
}
|
|
3609
|
-
if (routeKey === "admin") {
|
|
3610
|
-
const manifestMeta = marketplaceManifestCache.get(pluginId);
|
|
3611
|
-
if (manifestMeta?.admin?.pages?.length || manifestMeta?.admin?.widgets?.length) return { public: false };
|
|
3612
|
-
const entry = this.sandboxedPluginEntries.find((e) => e.id === pluginId);
|
|
3613
|
-
if (entry?.adminPages?.length || entry?.adminWidgets?.length) return { public: false };
|
|
3614
|
-
}
|
|
3615
|
-
if (this.findSandboxedPlugin(pluginId)) return { public: false };
|
|
3616
|
-
return null;
|
|
3617
|
-
}
|
|
3618
|
-
/**
|
|
3619
|
-
* Resolve the settings schema for a runtime-installed (marketplace or
|
|
3620
|
-
* registry) plugin from its cached manifest. Returns `{}` for a known
|
|
3621
|
-
* plugin without a schema and `null` for unknown plugins, matching the
|
|
3622
|
-
* contract of `getPluginSettingsSchema` for build-time plugins.
|
|
3623
|
-
*/
|
|
3624
|
-
getRuntimePluginSettingsSchema(pluginId) {
|
|
3625
|
-
const meta = marketplaceManifestCache.get(pluginId);
|
|
3626
|
-
if (!meta) return null;
|
|
3627
|
-
return meta.admin?.settingsSchema ?? {};
|
|
3628
|
-
}
|
|
3629
|
-
async handlePluginApiRoute(pluginId, method, path, request, user, invalidateContentCache, editorDispatch) {
|
|
3630
|
-
if (!this.isPluginEnabled(pluginId)) return {
|
|
3631
|
-
success: false,
|
|
3632
|
-
error: {
|
|
3633
|
-
code: "NOT_FOUND",
|
|
3634
|
-
message: `Plugin not enabled: ${pluginId}`
|
|
3635
|
-
}
|
|
3636
|
-
};
|
|
3637
|
-
const normalizedMethod = method.toUpperCase();
|
|
3638
|
-
const routeMeta = this.getPluginRouteMeta(pluginId, path);
|
|
3639
|
-
if (routeMeta?.methods && !routeMeta.methods.some((allowed) => allowed === normalizedMethod)) return {
|
|
3640
|
-
success: false,
|
|
3641
|
-
status: 405,
|
|
3642
|
-
error: {
|
|
3643
|
-
code: "METHOD_NOT_ALLOWED",
|
|
3644
|
-
message: "Method not allowed"
|
|
3645
|
-
}
|
|
3646
|
-
};
|
|
3647
|
-
const caller = user ? toRouteCallerInfo(user) : void 0;
|
|
3648
|
-
let body;
|
|
3649
|
-
try {
|
|
3650
|
-
body = await parseRouteInput(request, routeMeta?.request);
|
|
3651
|
-
} catch (error) {
|
|
3652
|
-
if (error instanceof PluginRouteRequestError) return {
|
|
3653
|
-
success: false,
|
|
3654
|
-
status: error.status,
|
|
3655
|
-
error: {
|
|
3656
|
-
code: "INVALID_PLUGIN_REQUEST",
|
|
3657
|
-
message: error.message
|
|
3658
|
-
}
|
|
3659
|
-
};
|
|
3660
|
-
throw error;
|
|
3661
|
-
}
|
|
3662
|
-
const routeKey = path.replace(LEADING_SLASH_PATTERN, "");
|
|
3663
|
-
const adminDefinition = !editorDispatch && routeKey === "admin" ? this.getSandboxedAdminDefinition(pluginId) : null;
|
|
3664
|
-
const uiResult = adminDefinition ? this.resolvePluginUiContext(adminDefinition, body, request) : {};
|
|
3665
|
-
if (uiResult.error) return {
|
|
3666
|
-
success: false,
|
|
3667
|
-
status: 400,
|
|
3668
|
-
error: uiResult.error
|
|
3669
|
-
};
|
|
3670
|
-
const trustedPlugin = this.configuredPlugins.find((p) => p.id === pluginId);
|
|
3671
|
-
if (trustedPlugin && this.enabledPlugins.has(trustedPlugin.id)) {
|
|
3672
|
-
const routeRegistry = new PluginRouteRegistry({
|
|
3673
|
-
...this.pipelineFactoryOptions,
|
|
3674
|
-
contentActions: this.contentActionsWithInvalidator(invalidateContentCache),
|
|
3675
|
-
emailPipeline: this.email ?? void 0,
|
|
3676
|
-
cronReschedule: () => this.cronScheduler?.reschedule(),
|
|
3677
|
-
trustedProxyHeaders: getTrustedProxyHeaders(this.config)
|
|
3678
|
-
});
|
|
3679
|
-
routeRegistry.register(trustedPlugin);
|
|
3680
|
-
const result = await routeRegistry.invoke(pluginId, routeKey, {
|
|
3681
|
-
request,
|
|
3682
|
-
body,
|
|
3683
|
-
user: caller,
|
|
3684
|
-
ui: editorDispatch?.ui ?? uiResult.context
|
|
3685
|
-
});
|
|
3686
|
-
return editorDispatch ? this.validatePluginEditorExtensionResponse(pluginId, editorDispatch, result) : adminDefinition ? this.validateSandboxedAdminResponse(pluginId, adminDefinition, result) : result;
|
|
3687
|
-
}
|
|
3688
|
-
const sandboxedPlugin = this.findSandboxedPlugin(pluginId);
|
|
3689
|
-
if (sandboxedPlugin) {
|
|
3690
|
-
const result = await this.handleSandboxedRoute(sandboxedPlugin, path, request, body, routeMeta ?? { public: false }, caller, editorDispatch?.ui ?? uiResult.context, invalidateContentCache);
|
|
3691
|
-
return editorDispatch ? this.validatePluginEditorExtensionResponse(pluginId, editorDispatch, result) : adminDefinition ? this.validateSandboxedAdminResponse(pluginId, adminDefinition, result) : result;
|
|
3692
|
-
}
|
|
3693
|
-
return {
|
|
3694
|
-
success: false,
|
|
3695
|
-
error: {
|
|
3696
|
-
code: "NOT_FOUND",
|
|
3697
|
-
message: `Plugin not found: ${pluginId}`
|
|
3698
|
-
}
|
|
3699
|
-
};
|
|
3700
|
-
}
|
|
3701
|
-
contentActionsWithInvalidator(invalidateContentCache) {
|
|
3702
|
-
const actions = this.pipelineFactoryOptions.contentActions;
|
|
3703
|
-
if (!actions || !invalidateContentCache) return actions;
|
|
3704
|
-
return {
|
|
3705
|
-
...actions,
|
|
3706
|
-
begin: (pluginId, invocationId, invocationInvalidator) => actions.begin?.(pluginId, invocationId, invocationInvalidator ?? invalidateContentCache),
|
|
3707
|
-
publish: (pluginId, collection, id, options, invocationId) => actions.publish(pluginId, collection, id, options, invocationId, invalidateContentCache),
|
|
3708
|
-
unpublish: (pluginId, collection, id, options, invocationId) => actions.unpublish(pluginId, collection, id, options, invocationId, invalidateContentCache),
|
|
3709
|
-
schedule: (pluginId, collection, id, options, invocationId) => actions.schedule(pluginId, collection, id, options, invocationId, invalidateContentCache),
|
|
3710
|
-
unschedule: (pluginId, collection, id, options, invocationId) => actions.unschedule(pluginId, collection, id, options, invocationId, invalidateContentCache),
|
|
3711
|
-
restore: (pluginId, collection, id, options, invocationId) => actions.restore(pluginId, collection, id, options, invocationId, invalidateContentCache)
|
|
3712
|
-
};
|
|
3713
|
-
}
|
|
3714
|
-
async getPluginMcpTools(pluginId) {
|
|
3715
|
-
const tools = [];
|
|
3716
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3717
|
-
for (const plugin of this.configuredPlugins) {
|
|
3718
|
-
if (pluginId && plugin.id !== pluginId) continue;
|
|
3719
|
-
for (const [name, tool] of Object.entries(plugin.mcp?.tools ?? {})) {
|
|
3720
|
-
const route = plugin.routes[tool.route];
|
|
3721
|
-
if (!route || route.public || route.response === "raw" || !isJsonPostRouteContract(route) || !route.permission || !Object.hasOwn(Permissions, route.permission)) continue;
|
|
3722
|
-
const key = `${plugin.id}__${name}`;
|
|
3723
|
-
if (seen.has(key)) continue;
|
|
3724
|
-
seen.add(key);
|
|
3725
|
-
tools.push({
|
|
3726
|
-
pluginId: plugin.id,
|
|
3727
|
-
name,
|
|
3728
|
-
description: tool.description,
|
|
3729
|
-
route: tool.route,
|
|
3730
|
-
permission: route.permission,
|
|
3731
|
-
destructive: tool.destructive ?? false,
|
|
3732
|
-
inputSchema: tool.input,
|
|
3733
|
-
outputSchema: tool.output
|
|
3734
|
-
});
|
|
3735
|
-
}
|
|
3736
|
-
}
|
|
3737
|
-
const addManifestTools = (id, mcp) => {
|
|
3738
|
-
if (pluginId && id !== pluginId) return;
|
|
3739
|
-
for (const tool of mcp?.tools ?? []) {
|
|
3740
|
-
const key = `${id}__${tool.name}`;
|
|
3741
|
-
const routeMeta = this.getPluginRouteMeta(id, tool.route);
|
|
3742
|
-
if (seen.has(key) || !routeMeta || routeMeta.public || routeMeta.response === "raw" || !isJsonPostRouteContract(routeMeta) || routeMeta.permission !== tool.permission || !Object.hasOwn(Permissions, tool.permission)) continue;
|
|
3743
|
-
seen.add(key);
|
|
3744
|
-
tools.push({
|
|
3745
|
-
pluginId: id,
|
|
3746
|
-
name: tool.name,
|
|
3747
|
-
description: tool.description,
|
|
3748
|
-
route: tool.route,
|
|
3749
|
-
permission: tool.permission,
|
|
3750
|
-
destructive: tool.destructive,
|
|
3751
|
-
inputSchema: z.fromJSONSchema({ ...tool.inputSchema }),
|
|
3752
|
-
outputSchema: tool.outputSchema ? z.fromJSONSchema({ ...tool.outputSchema }) : void 0
|
|
3753
|
-
});
|
|
3754
|
-
}
|
|
3755
|
-
};
|
|
3756
|
-
for (const entry of this.sandboxedPluginEntries) addManifestTools(entry.id, entry.mcp);
|
|
3757
|
-
for (const [id, manifest] of marketplaceManifestCache) addManifestTools(id, manifest.mcp);
|
|
3758
|
-
return tools;
|
|
3759
|
-
}
|
|
3760
|
-
async getEnabledPluginMcpTools() {
|
|
3761
|
-
const [tools, states] = await Promise.all([this.getPluginMcpTools(), new PluginStateRepository(this.db).getAll()]);
|
|
3762
|
-
const stateByPlugin = new Map(states.map((state) => [state.pluginId, state]));
|
|
3763
|
-
return tools.filter((tool) => {
|
|
3764
|
-
const state = stateByPlugin.get(tool.pluginId);
|
|
3765
|
-
if (!state?.mcpToolsEnabled || state.status !== "active" || !this.isPluginEnabled(tool.pluginId)) return false;
|
|
3766
|
-
return state.mcpToolsConsent === this.serializePluginMcpConsent(tools, tool.pluginId);
|
|
3767
|
-
});
|
|
3768
|
-
}
|
|
3769
|
-
serializePluginMcpConsent(tools, pluginId) {
|
|
3770
|
-
return JSON.stringify(tools.filter((tool) => tool.pluginId === pluginId).map((tool) => ({
|
|
3771
|
-
name: tool.name,
|
|
3772
|
-
description: tool.description,
|
|
3773
|
-
route: tool.route,
|
|
3774
|
-
permission: tool.permission,
|
|
3775
|
-
destructive: tool.destructive,
|
|
3776
|
-
inputSchema: z.toJSONSchema(tool.inputSchema, { target: "draft-7" }),
|
|
3777
|
-
...tool.outputSchema ? { outputSchema: z.toJSONSchema(tool.outputSchema, { target: "draft-7" }) } : {}
|
|
3778
|
-
})).toSorted((a, b) => a.name.localeCompare(b.name)));
|
|
3779
|
-
}
|
|
3780
|
-
async handlePluginMcpTool(pluginId, toolName, route, input, actorId, request, caller, invalidateContentCache) {
|
|
3781
|
-
const requestMeta = extractRequestMeta(request, getTrustedProxyHeaders(this.config));
|
|
3782
|
-
const audit = new AuditRepository(this.db);
|
|
3783
|
-
const headers = new Headers(request.headers);
|
|
3784
|
-
headers.delete("content-length");
|
|
3785
|
-
headers.delete("content-encoding");
|
|
3786
|
-
const internalRequest = new Request(request.url, {
|
|
3787
|
-
method: "POST",
|
|
3788
|
-
headers,
|
|
3789
|
-
body: JSON.stringify(input)
|
|
3790
|
-
});
|
|
3791
|
-
const result = await this.handlePluginApiRoute(pluginId, "POST", route, internalRequest, caller, invalidateContentCache);
|
|
3792
|
-
await audit.log({
|
|
3793
|
-
actorId,
|
|
3794
|
-
actorIp: requestMeta.ip ?? void 0,
|
|
3795
|
-
action: "plugin_tool_invoke",
|
|
3796
|
-
resourceType: "plugin_mcp_tool",
|
|
3797
|
-
resourceId: `${pluginId}__${toolName}`,
|
|
3798
|
-
details: {
|
|
3799
|
-
pluginId,
|
|
3800
|
-
tool: toolName,
|
|
3801
|
-
route
|
|
3802
|
-
},
|
|
3803
|
-
status: result.success ? "success" : "failure"
|
|
3804
|
-
});
|
|
3805
|
-
return result;
|
|
3806
|
-
}
|
|
3807
|
-
async handlePluginMcpDenied(pluginId, toolName, route, actorId, request, reason) {
|
|
3808
|
-
const requestMeta = extractRequestMeta(request, getTrustedProxyHeaders(this.config));
|
|
3809
|
-
await new AuditRepository(this.db).log({
|
|
3810
|
-
actorId,
|
|
3811
|
-
actorIp: requestMeta.ip ?? void 0,
|
|
3812
|
-
action: "plugin_tool_invoke",
|
|
3813
|
-
resourceType: "plugin_mcp_tool",
|
|
3814
|
-
resourceId: `${pluginId}__${toolName}`,
|
|
3815
|
-
details: {
|
|
3816
|
-
pluginId,
|
|
3817
|
-
tool: toolName,
|
|
3818
|
-
route,
|
|
3819
|
-
reason
|
|
3820
|
-
},
|
|
3821
|
-
status: "denied"
|
|
3822
|
-
});
|
|
3823
|
-
}
|
|
3824
|
-
findSandboxedPlugin(pluginId) {
|
|
3825
|
-
for (const [key, plugin] of this.sandboxedPlugins) if (key.startsWith(pluginId + ":")) return plugin;
|
|
3826
|
-
}
|
|
3827
|
-
/**
|
|
3828
|
-
* Drop incoming keys that have no matching collection field when the entry
|
|
3829
|
-
* already stores them in its live data or current draft revision.
|
|
3830
|
-
*
|
|
3831
|
-
* Draft revisions keep the full `data` JSON, so deleting a field can strand
|
|
3832
|
-
* the old value; this lets a read-then-write save succeed without allowing
|
|
3833
|
-
* genuinely unknown keys.
|
|
3834
|
-
*/
|
|
3835
|
-
async dropUnknownKeysAlreadyStored(data, existing, knownFieldSlugs) {
|
|
3836
|
-
if (!existing) return data;
|
|
3837
|
-
let stored = existing.data ?? {};
|
|
3838
|
-
if (existing.draftRevisionId) {
|
|
3839
|
-
const draft = await new RevisionRepository(this.db).findById(existing.draftRevisionId).catch(() => null);
|
|
3840
|
-
if (draft?.data) stored = draft.data;
|
|
3841
|
-
}
|
|
3842
|
-
const stale = staleStoredKeys(data, stored, knownFieldSlugs);
|
|
3843
|
-
if (stale.length === 0) return data;
|
|
3844
|
-
const result = { ...data };
|
|
3845
|
-
for (const key of stale) delete result[key];
|
|
3846
|
-
return result;
|
|
3847
|
-
}
|
|
3848
|
-
/**
|
|
3849
|
-
* Normalize field values in content data before validation.
|
|
3850
|
-
* Turns a blank string in an array-valued field into `null`, and fills
|
|
3851
|
-
* missing image/file dimensions, storageKey, mimeType, and filename from providers.
|
|
3852
|
-
*/
|
|
3853
|
-
async normalizeFieldValues(collection, data, preloaded, preloadedBlockTypes, includeBlocks = true) {
|
|
3854
|
-
let collectionInfo = preloaded;
|
|
3855
|
-
if (collectionInfo === void 0) try {
|
|
3856
|
-
collectionInfo = await this.schemaRegistry.getCollectionWithFields(collection);
|
|
3857
|
-
} catch {
|
|
3858
|
-
return data;
|
|
3859
|
-
}
|
|
3860
|
-
if (!collectionInfo?.fields) return data;
|
|
3861
|
-
const result = { ...data };
|
|
3862
|
-
for (const field of collectionInfo.fields) {
|
|
3863
|
-
const value = result[field.slug];
|
|
3864
|
-
if (ARRAY_FIELD_TYPES.has(field.type) && typeof value === "string" && !value.trim()) result[field.slug] = null;
|
|
3865
|
-
}
|
|
3866
|
-
const imageFields = collectionInfo.fields.filter((f) => f.type === "image" || f.type === "file");
|
|
3867
|
-
const repeaterFields = collectionInfo.fields.filter((f) => f.type === "repeater" && Array.isArray(f.validation?.subFields));
|
|
3868
|
-
const blockFields = includeBlocks ? collectionInfo.fields.filter((field) => field.type === "blocks") : [];
|
|
3869
|
-
if (imageFields.length === 0 && repeaterFields.length === 0 && blockFields.length === 0) return result;
|
|
3870
|
-
const getProvider = (id) => this.getMediaProvider(id);
|
|
3871
|
-
for (const field of imageFields) {
|
|
3872
|
-
const value = result[field.slug];
|
|
3873
|
-
if (value == null) continue;
|
|
3874
|
-
try {
|
|
3875
|
-
const normalized = field.type === "image" ? await normalizeImageValue(value, getProvider) : await normalizeMediaValue(value, getProvider);
|
|
3876
|
-
if (normalized) result[field.slug] = normalized;
|
|
3877
|
-
} catch {}
|
|
3878
|
-
}
|
|
3879
|
-
for (const field of repeaterFields) {
|
|
3880
|
-
const value = result[field.slug];
|
|
3881
|
-
if (!Array.isArray(value)) continue;
|
|
3882
|
-
const mediaSubFieldSlugs = (field.validation?.subFields ?? []).filter((sub) => sub.type === "image").map((sub) => sub.slug);
|
|
3883
|
-
if (mediaSubFieldSlugs.length === 0) continue;
|
|
3884
|
-
const items = value;
|
|
3885
|
-
result[field.slug] = await Promise.all(items.map(async (item) => {
|
|
3886
|
-
if (!isRecord(item)) return item;
|
|
3887
|
-
const normalizedItem = { ...item };
|
|
3888
|
-
for (const slug of mediaSubFieldSlugs) {
|
|
3889
|
-
const subValue = normalizedItem[slug];
|
|
3890
|
-
if (subValue == null) continue;
|
|
3891
|
-
try {
|
|
3892
|
-
const normalized = await normalizeImageValue(subValue, getProvider);
|
|
3893
|
-
if (normalized) normalizedItem[slug] = normalized;
|
|
3894
|
-
} catch {}
|
|
3895
|
-
}
|
|
3896
|
-
return normalizedItem;
|
|
3897
|
-
}));
|
|
3898
|
-
}
|
|
3899
|
-
if (blockFields.length > 0) {
|
|
3900
|
-
const blockTypes = preloadedBlockTypes ?? await resolveBlockTypes(this.db);
|
|
3901
|
-
for (const field of blockFields) {
|
|
3902
|
-
const value = result[field.slug];
|
|
3903
|
-
if (!Array.isArray(value)) continue;
|
|
3904
|
-
result[field.slug] = await Promise.all(value.map(async (block) => {
|
|
3905
|
-
if (!isRecord(block) || typeof block._type !== "string") return block;
|
|
3906
|
-
const version = blockTypes.get(block._type)?.versions.find((candidate) => candidate.version === block._version);
|
|
3907
|
-
if (!version || version.unsupportedTypes?.length) return block;
|
|
3908
|
-
const normalizedBlock = { ...block };
|
|
3909
|
-
for (const nestedField of version.fields) {
|
|
3910
|
-
const nestedValue = normalizedBlock[nestedField.slug];
|
|
3911
|
-
if (nestedValue == null) continue;
|
|
3912
|
-
try {
|
|
3913
|
-
if (nestedField.type === "image") {
|
|
3914
|
-
const normalized = await normalizeImageValue(nestedValue, getProvider);
|
|
3915
|
-
if (normalized) normalizedBlock[nestedField.slug] = normalized;
|
|
3916
|
-
} else if (nestedField.type === "file") {
|
|
3917
|
-
const normalized = await normalizeMediaValue(nestedValue, getProvider);
|
|
3918
|
-
if (normalized) normalizedBlock[nestedField.slug] = normalized;
|
|
3919
|
-
} else if (nestedField.type === "repeater" && Array.isArray(nestedValue)) {
|
|
3920
|
-
const imageSlugs = (nestedField.validation?.subFields ?? []).filter((subField) => subField.type === "image").map((subField) => subField.slug);
|
|
3921
|
-
normalizedBlock[nestedField.slug] = await Promise.all(nestedValue.map(async (item) => {
|
|
3922
|
-
if (!isRecord(item)) return item;
|
|
3923
|
-
const normalizedItem = { ...item };
|
|
3924
|
-
for (const slug of imageSlugs) try {
|
|
3925
|
-
const normalized = await normalizeImageValue(normalizedItem[slug], getProvider);
|
|
3926
|
-
if (normalized) normalizedItem[slug] = normalized;
|
|
3927
|
-
} catch {
|
|
3928
|
-
continue;
|
|
3929
|
-
}
|
|
3930
|
-
return normalizedItem;
|
|
3931
|
-
}));
|
|
3932
|
-
}
|
|
3933
|
-
} catch {
|
|
3934
|
-
continue;
|
|
3935
|
-
}
|
|
3936
|
-
}
|
|
3937
|
-
return normalizedBlock;
|
|
3938
|
-
}));
|
|
3939
|
-
}
|
|
3940
|
-
}
|
|
3941
|
-
return result;
|
|
3942
|
-
}
|
|
3943
|
-
runAfterSaveHooks(content, collection, isNew, actor, excludePluginId) {
|
|
3944
|
-
after(async () => {
|
|
3945
|
-
if (this.hooks.hasHooks("content:afterSave")) try {
|
|
3946
|
-
await this.hooks.runContentAfterSave(content, collection, isNew, actor, excludePluginId);
|
|
3947
|
-
} catch (err) {
|
|
3948
|
-
console.error("EmDash afterSave hook error:", err);
|
|
3949
|
-
}
|
|
3950
|
-
});
|
|
3951
|
-
}
|
|
3952
|
-
runAfterDeleteHooks(id, collection, permanent) {
|
|
3953
|
-
after(async () => {
|
|
3954
|
-
if (this.hooks.hasHooks("content:afterDelete")) try {
|
|
3955
|
-
await this.hooks.runContentAfterDelete(id, collection, permanent);
|
|
3956
|
-
} catch (err) {
|
|
3957
|
-
console.error("EmDash afterDelete hook error:", err);
|
|
3958
|
-
}
|
|
3959
|
-
});
|
|
3960
|
-
}
|
|
3961
|
-
runDeferredContentHook(name, content, collection, afterPluginId, pluginInvocationId) {
|
|
3962
|
-
const label = name.slice(8);
|
|
3963
|
-
const invoke = async () => {
|
|
3964
|
-
if (this.hooks.hasHooks(name)) try {
|
|
3965
|
-
switch (name) {
|
|
3966
|
-
case "content:afterPublish":
|
|
3967
|
-
await this.hooks.runContentAfterPublish(content, collection);
|
|
3968
|
-
break;
|
|
3969
|
-
case "content:afterUnpublish":
|
|
3970
|
-
await this.hooks.runContentAfterUnpublish(content, collection);
|
|
3971
|
-
break;
|
|
3972
|
-
case "content:afterRestore":
|
|
3973
|
-
await this.hooks.runContentAfterRestore(content, collection);
|
|
3974
|
-
break;
|
|
3975
|
-
case "content:afterSchedule":
|
|
3976
|
-
await this.hooks.runContentAfterSchedule(content, collection);
|
|
3977
|
-
break;
|
|
3978
|
-
case "content:afterUnschedule":
|
|
3979
|
-
await this.hooks.runContentAfterUnschedule(content, collection);
|
|
3980
|
-
break;
|
|
3981
|
-
}
|
|
3982
|
-
} catch (err) {
|
|
3983
|
-
console.error(`EmDash ${label} hook error:`, err);
|
|
3984
|
-
}
|
|
3985
|
-
};
|
|
3986
|
-
const contentId = typeof content.id === "string" ? content.id : void 0;
|
|
3987
|
-
const guardKey = afterPluginId && contentId ? this.pluginContentActionKey(collection, contentId) : void 0;
|
|
3988
|
-
const invokeWithGuard = async () => {
|
|
3989
|
-
if (guardKey) this.retainPluginContentAction(guardKey);
|
|
3990
|
-
try {
|
|
3991
|
-
await invoke();
|
|
3992
|
-
} finally {
|
|
3993
|
-
if (guardKey) this.releasePluginContentAction(guardKey);
|
|
3994
|
-
}
|
|
3995
|
-
};
|
|
3996
|
-
if (afterPluginId && pluginInvocationId && this.findSandboxedPlugin(afterPluginId)) {
|
|
3997
|
-
const key = this.pluginInvocationKey(afterPluginId, pluginInvocationId);
|
|
3998
|
-
if (!this.activePluginInvocations.has(key) || this.releasedPluginInvocations.has(key)) {
|
|
3999
|
-
after(invokeWithGuard);
|
|
4000
|
-
return;
|
|
4001
|
-
}
|
|
4002
|
-
const pending = this.pendingPluginAfterHooks.get(key) ?? [];
|
|
4003
|
-
pending.push(invokeWithGuard);
|
|
4004
|
-
this.pendingPluginAfterHooks.set(key, pending);
|
|
4005
|
-
return;
|
|
4006
|
-
}
|
|
4007
|
-
after(invokeWithGuard);
|
|
4008
|
-
}
|
|
4009
|
-
pluginInvocationKey(pluginId, invocationId) {
|
|
4010
|
-
return JSON.stringify([pluginId, invocationId]);
|
|
4011
|
-
}
|
|
4012
|
-
beginPluginInvocation(pluginId, invocationId, invalidateContentCache) {
|
|
4013
|
-
const key = this.pluginInvocationKey(pluginId, invocationId);
|
|
4014
|
-
this.pendingPluginAfterHooks.delete(key);
|
|
4015
|
-
this.releasedPluginInvocations.delete(key);
|
|
4016
|
-
if (invalidateContentCache) this.pluginInvocationCacheInvalidators.set(key, invalidateContentCache);
|
|
4017
|
-
else this.pluginInvocationCacheInvalidators.delete(key);
|
|
4018
|
-
this.activePluginInvocations.add(key);
|
|
4019
|
-
}
|
|
4020
|
-
flushPluginAfterHooks(pluginId, invocationId, final = true) {
|
|
4021
|
-
if (!invocationId) return Promise.resolve();
|
|
4022
|
-
const key = this.pluginInvocationKey(pluginId, invocationId);
|
|
4023
|
-
this.releasedPluginInvocations.add(key);
|
|
4024
|
-
const pending = this.pendingPluginAfterHooks.get(key) ?? [];
|
|
4025
|
-
this.pendingPluginAfterHooks.delete(key);
|
|
4026
|
-
for (const invoke of pending) after(invoke);
|
|
4027
|
-
if (final) {
|
|
4028
|
-
this.releasedPluginInvocations.delete(key);
|
|
4029
|
-
this.activePluginInvocations.delete(key);
|
|
4030
|
-
this.pluginInvocationCacheInvalidators.delete(key);
|
|
4031
|
-
} else {
|
|
4032
|
-
const timer = setTimeout(() => {
|
|
4033
|
-
this.releasedPluginInvocations.delete(key);
|
|
4034
|
-
this.activePluginInvocations.delete(key);
|
|
4035
|
-
this.pluginInvocationCacheInvalidators.delete(key);
|
|
4036
|
-
}, PLUGIN_INVOCATION_RELEASE_GRACE_MS);
|
|
4037
|
-
if (typeof timer === "object") timer.unref();
|
|
4038
|
-
}
|
|
4039
|
-
return Promise.resolve();
|
|
4040
|
-
}
|
|
4041
|
-
runAfterPublishHooks(content, collection, afterPluginId, pluginInvocationId) {
|
|
4042
|
-
this.runDeferredContentHook("content:afterPublish", content, collection, afterPluginId, pluginInvocationId);
|
|
4043
|
-
}
|
|
4044
|
-
runAfterUnpublishHooks(content, collection, afterPluginId, pluginInvocationId) {
|
|
4045
|
-
this.runDeferredContentHook("content:afterUnpublish", content, collection, afterPluginId, pluginInvocationId);
|
|
4046
|
-
}
|
|
4047
|
-
runAfterRestoreHooks(content, collection, afterPluginId, pluginInvocationId) {
|
|
4048
|
-
this.runDeferredContentHook("content:afterRestore", content, collection, afterPluginId, pluginInvocationId);
|
|
4049
|
-
}
|
|
4050
|
-
runAfterScheduleHooks(content, collection, afterPluginId, pluginInvocationId) {
|
|
4051
|
-
this.runDeferredContentHook("content:afterSchedule", content, collection, afterPluginId, pluginInvocationId);
|
|
4052
|
-
}
|
|
4053
|
-
runAfterUnscheduleHooks(content, collection, afterPluginId, pluginInvocationId) {
|
|
4054
|
-
this.runDeferredContentHook("content:afterUnschedule", content, collection, afterPluginId, pluginInvocationId);
|
|
4055
|
-
}
|
|
4056
|
-
async handleSandboxedRoute(plugin, path, request, body, routeMeta, user, ui, invalidateContentCache) {
|
|
4057
|
-
const routeName = path.replace(LEADING_SLASH_PATTERN, "");
|
|
4058
|
-
try {
|
|
4059
|
-
const declaredHeaders = routeMeta.request ? routeMeta.request.headers ?? [] : void 0;
|
|
4060
|
-
const headers = sanitizeHeadersForSandbox(request.headers, declaredHeaders);
|
|
4061
|
-
const meta = extractRequestMeta(request, this.config);
|
|
4062
|
-
return {
|
|
4063
|
-
success: true,
|
|
4064
|
-
data: await plugin.invokeRoute(routeName, body, {
|
|
4065
|
-
url: request.url,
|
|
4066
|
-
method: request.method,
|
|
4067
|
-
headers,
|
|
4068
|
-
meta,
|
|
4069
|
-
user,
|
|
4070
|
-
ui
|
|
4071
|
-
}, { invalidateContentCache })
|
|
4072
|
-
};
|
|
4073
|
-
} catch (error) {
|
|
4074
|
-
if (error instanceof PluginRouteRequestError) return {
|
|
4075
|
-
success: false,
|
|
4076
|
-
status: error.status,
|
|
4077
|
-
error: {
|
|
4078
|
-
code: "INVALID_PLUGIN_REQUEST",
|
|
4079
|
-
message: error.message
|
|
4080
|
-
}
|
|
4081
|
-
};
|
|
4082
|
-
console.error(`EmDash: Sandboxed plugin route error:`, error);
|
|
4083
|
-
const sandboxRouteError = getSandboxRouteErrorDetails(error);
|
|
4084
|
-
if (sandboxRouteError) return {
|
|
4085
|
-
success: false,
|
|
4086
|
-
status: sandboxRouteError.status,
|
|
4087
|
-
error: {
|
|
4088
|
-
code: sandboxRouteError.code,
|
|
4089
|
-
message: sandboxRouteError.message
|
|
4090
|
-
}
|
|
4091
|
-
};
|
|
4092
|
-
return {
|
|
4093
|
-
success: false,
|
|
4094
|
-
error: {
|
|
4095
|
-
code: "ROUTE_ERROR",
|
|
4096
|
-
message: error instanceof Error ? error.message : String(error)
|
|
4097
|
-
}
|
|
4098
|
-
};
|
|
4099
|
-
}
|
|
4100
|
-
}
|
|
4101
|
-
/**
|
|
4102
|
-
* Cache for page contributions. Uses a WeakMap keyed on the PublicPageContext
|
|
4103
|
-
* object so results are collected once per page context per request, even when
|
|
4104
|
-
* multiple render components (EmDashHead, EmDashBodyStart, EmDashBodyEnd)
|
|
4105
|
-
* request contributions from the same page.
|
|
4106
|
-
*/
|
|
4107
|
-
pageContributionCache = /* @__PURE__ */ new WeakMap();
|
|
4108
|
-
/**
|
|
4109
|
-
* Collect all page contributions (metadata + fragments) in a single pass.
|
|
4110
|
-
* Results are cached by page context object identity.
|
|
4111
|
-
*/
|
|
4112
|
-
async collectPageContributions(page) {
|
|
4113
|
-
const cached = this.pageContributionCache.get(page);
|
|
4114
|
-
if (cached) return cached;
|
|
4115
|
-
const promise = this.doCollectPageContributions(page);
|
|
4116
|
-
this.pageContributionCache.set(page, promise);
|
|
4117
|
-
return promise;
|
|
4118
|
-
}
|
|
4119
|
-
async doCollectPageContributions(page) {
|
|
4120
|
-
const metadata = [];
|
|
4121
|
-
const fragments = [];
|
|
4122
|
-
if (this.hooks.hasHooks("page:metadata")) {
|
|
4123
|
-
const results = await this.hooks.runPageMetadata({ page });
|
|
4124
|
-
for (const r of results) metadata.push(...r.contributions);
|
|
4125
|
-
}
|
|
4126
|
-
if (this.hooks.hasHooks("page:fragments")) {
|
|
4127
|
-
const results = await this.hooks.runPageFragments({ page });
|
|
4128
|
-
for (const r of results) fragments.push(...r.contributions);
|
|
4129
|
-
}
|
|
4130
|
-
return {
|
|
4131
|
-
metadata,
|
|
4132
|
-
fragments
|
|
4133
|
-
};
|
|
4134
|
-
}
|
|
4135
|
-
/**
|
|
4136
|
-
* Collect page metadata contributions from trusted and sandboxed plugins.
|
|
4137
|
-
* Delegates to the single-pass collector and returns the metadata portion.
|
|
4138
|
-
*/
|
|
4139
|
-
async collectPageMetadata(page) {
|
|
4140
|
-
const { metadata } = await this.collectPageContributions(page);
|
|
4141
|
-
return metadata;
|
|
4142
|
-
}
|
|
4143
|
-
/**
|
|
4144
|
-
* Collect page fragment contributions from trusted plugins only.
|
|
4145
|
-
* Delegates to the single-pass collector and returns the fragments portion.
|
|
4146
|
-
*/
|
|
4147
|
-
async collectPageFragments(page) {
|
|
4148
|
-
const { fragments } = await this.collectPageContributions(page);
|
|
4149
|
-
return fragments;
|
|
4150
|
-
}
|
|
4151
|
-
isPluginEnabled(pluginId) {
|
|
4152
|
-
const status = this.pluginStates.get(pluginId);
|
|
4153
|
-
return status === void 0 || status === "active";
|
|
4154
|
-
}
|
|
4155
|
-
};
|
|
4156
|
-
/**
|
|
4157
|
-
* Normalize an image field value together with its `darkVariant`. The media
|
|
4158
|
-
* normalizer only keeps the media item's own keys, so the variant is normalized
|
|
4159
|
-
* separately and reattached.
|
|
4160
|
-
*/
|
|
4161
|
-
async function normalizeImageValue(value, getProvider) {
|
|
4162
|
-
const primary = await normalizePrimaryImageValue(value, getProvider);
|
|
4163
|
-
if (!primary || !isRecord(value) || value.darkVariant == null) return primary;
|
|
4164
|
-
const darkVariant = await normalizeMediaValue(value.darkVariant, getProvider);
|
|
4165
|
-
return darkVariant ? {
|
|
4166
|
-
...primary,
|
|
4167
|
-
darkVariant
|
|
4168
|
-
} : primary;
|
|
4169
|
-
}
|
|
4170
|
-
/**
|
|
4171
|
-
* Normalize the primary image of an image field value.
|
|
4172
|
-
*
|
|
4173
|
-
* A legacy string URL that the admin upgraded to `{ id: "", src: url }` so it
|
|
4174
|
-
* can carry a dark variant still has to normalize as a string: as an object it
|
|
4175
|
-
* counts as local media, which strips `src` and leaves nothing behind. The
|
|
4176
|
-
* upgrade outlives the variant — an editor can add one and remove it again —
|
|
4177
|
-
* so the shape decides, not the presence of `darkVariant`.
|
|
4178
|
-
*/
|
|
4179
|
-
async function normalizePrimaryImageValue(value, getProvider) {
|
|
4180
|
-
if (isRecord(value) && !value.id && typeof value.src === "string" && !value.provider) return normalizeMediaValue(value.src, getProvider);
|
|
4181
|
-
return normalizeMediaValue(value, getProvider);
|
|
4182
|
-
}
|
|
4183
|
-
|
|
4184
|
-
//#endregion
|
|
4185
|
-
export { EmDashRuntime as n, DB_INIT_DEADLINE_MS as t };
|
|
4186
|
-
//# sourceMappingURL=emdash-runtime-DAjghyEG.mjs.map
|