emdash 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{adapters-BktHA7EO.d.mts → adapters-9DybjTO6.d.mts} +1 -1
- package/dist/{adapters-BktHA7EO.d.mts.map → adapters-9DybjTO6.d.mts.map} +1 -1
- package/dist/allowed-origins-CDdG-4Gd.mjs +116 -0
- package/dist/allowed-origins-CDdG-4Gd.mjs.map +1 -0
- package/dist/api/route-utils.d.mts +68 -0
- package/dist/api/route-utils.d.mts.map +1 -0
- package/dist/api/route-utils.mjs +44 -0
- package/dist/api/route-utils.mjs.map +1 -0
- package/dist/api/schemas/index.d.mts +2 -0
- package/dist/api/schemas/index.mjs +4 -0
- package/dist/api-ayIQ7rIe.mjs +3941 -0
- package/dist/api-ayIQ7rIe.mjs.map +1 -0
- package/dist/api-tokens-D3C9v02m.mjs +3 -0
- package/dist/api-tokens-eYymBhIT.mjs +153 -0
- package/dist/api-tokens-eYymBhIT.mjs.map +1 -0
- package/dist/{apply-C1ZORgcy.mjs → apply-v4DBgjPw.mjs} +19 -346
- package/dist/apply-v4DBgjPw.mjs.map +1 -0
- package/dist/astro/index.d.mts +10 -6
- package/dist/astro/index.d.mts.map +1 -1
- package/dist/astro/index.mjs +42 -83
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +9 -5
- package/dist/astro/middleware/auth.d.mts.map +1 -1
- package/dist/astro/middleware/auth.mjs +25 -65
- package/dist/astro/middleware/auth.mjs.map +1 -1
- package/dist/astro/middleware/redirect.mjs +5 -5
- package/dist/astro/middleware/request-context.mjs +4 -4
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +140 -69
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/routes/PluginRegistry.d.mts +15 -0
- package/dist/astro/routes/PluginRegistry.d.mts.map +1 -0
- package/dist/astro/routes/PluginRegistry.mjs +25 -0
- package/dist/astro/routes/PluginRegistry.mjs.map +1 -0
- package/dist/astro/routes/api/admin/allowed-domains/_domain_.d.mts +15 -0
- package/dist/astro/routes/api/admin/allowed-domains/_domain_.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +67 -0
- package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs.map +1 -0
- package/dist/astro/routes/api/admin/allowed-domains/index.d.mts +15 -0
- package/dist/astro/routes/api/admin/allowed-domains/index.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/allowed-domains/index.mjs +67 -0
- package/dist/astro/routes/api/admin/allowed-domains/index.mjs.map +1 -0
- package/dist/astro/routes/api/admin/api-tokens/_id_.d.mts +11 -0
- package/dist/astro/routes/api/admin/api-tokens/_id_.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +33 -0
- package/dist/astro/routes/api/admin/api-tokens/_id_.mjs.map +1 -0
- package/dist/astro/routes/api/admin/api-tokens/index.d.mts +17 -0
- package/dist/astro/routes/api/admin/api-tokens/index.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/api-tokens/index.mjs +52 -0
- package/dist/astro/routes/api/admin/api-tokens/index.mjs.map +1 -0
- package/dist/astro/routes/api/admin/bylines/_id_/index.d.mts +10 -0
- package/dist/astro/routes/api/admin/bylines/_id_/index.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +74 -0
- package/dist/astro/routes/api/admin/bylines/_id_/index.mjs.map +1 -0
- package/dist/astro/routes/api/admin/bylines/index.d.mts +9 -0
- package/dist/astro/routes/api/admin/bylines/index.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/bylines/index.mjs +61 -0
- package/dist/astro/routes/api/admin/bylines/index.mjs.map +1 -0
- package/dist/astro/routes/api/admin/comments/_id_/status.d.mts +8 -0
- package/dist/astro/routes/api/admin/comments/_id_/status.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/comments/_id_/status.mjs +80 -0
- package/dist/astro/routes/api/admin/comments/_id_/status.mjs.map +1 -0
- package/dist/astro/routes/api/admin/comments/_id_.d.mts +15 -0
- package/dist/astro/routes/api/admin/comments/_id_.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/comments/_id_.mjs +47 -0
- package/dist/astro/routes/api/admin/comments/_id_.mjs.map +1 -0
- package/dist/astro/routes/api/admin/comments/bulk.d.mts +8 -0
- package/dist/astro/routes/api/admin/comments/bulk.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/comments/bulk.mjs +36 -0
- package/dist/astro/routes/api/admin/comments/bulk.mjs.map +1 -0
- package/dist/astro/routes/api/admin/comments/counts.d.mts +8 -0
- package/dist/astro/routes/api/admin/comments/counts.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/comments/counts.mjs +25 -0
- package/dist/astro/routes/api/admin/comments/counts.mjs.map +1 -0
- package/dist/astro/routes/api/admin/comments/index.d.mts +11 -0
- package/dist/astro/routes/api/admin/comments/index.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/comments/index.mjs +40 -0
- package/dist/astro/routes/api/admin/comments/index.mjs.map +1 -0
- package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.d.mts +8 -0
- package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +48 -0
- package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs.map +1 -0
- package/dist/astro/routes/api/admin/hooks/exclusive/index.d.mts +8 -0
- package/dist/astro/routes/api/admin/hooks/exclusive/index.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +36 -0
- package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs.map +1 -0
- package/dist/astro/routes/api/admin/oauth-clients/_id_.d.mts +19 -0
- package/dist/astro/routes/api/admin/oauth-clients/_id_.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +69 -0
- package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs.map +1 -0
- package/dist/astro/routes/api/admin/oauth-clients/index.d.mts +15 -0
- package/dist/astro/routes/api/admin/oauth-clients/index.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/oauth-clients/index.mjs +50 -0
- package/dist/astro/routes/api/admin/oauth-clients/index.mjs.map +1 -0
- package/dist/astro/routes/api/admin/plugins/_id_/disable.d.mts +8 -0
- package/dist/astro/routes/api/admin/plugins/_id_/disable.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +56 -0
- package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs.map +1 -0
- package/dist/astro/routes/api/admin/plugins/_id_/enable.d.mts +8 -0
- package/dist/astro/routes/api/admin/plugins/_id_/enable.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +59 -0
- package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs.map +1 -0
- package/dist/astro/routes/api/admin/plugins/_id_/index.d.mts +8 -0
- package/dist/astro/routes/api/admin/plugins/_id_/index.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +51 -0
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs.map +1 -0
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.d.mts +8 -0
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +58 -0
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs.map +1 -0
- package/dist/astro/routes/api/admin/plugins/_id_/update.d.mts +8 -0
- package/dist/astro/routes/api/admin/plugins/_id_/update.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +66 -0
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs.map +1 -0
- package/dist/astro/routes/api/admin/plugins/index.d.mts +8 -0
- package/dist/astro/routes/api/admin/plugins/index.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/plugins/index.mjs +49 -0
- package/dist/astro/routes/api/admin/plugins/index.mjs.map +1 -0
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.d.mts +8 -0
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +39 -0
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs.map +1 -0
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.d.mts +8 -0
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +51 -0
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs.map +1 -0
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.d.mts +8 -0
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +69 -0
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs.map +1 -0
- package/dist/astro/routes/api/admin/plugins/marketplace/index.d.mts +8 -0
- package/dist/astro/routes/api/admin/plugins/marketplace/index.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +58 -0
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs.map +1 -0
- package/dist/astro/routes/api/admin/plugins/registry/install.d.mts +8 -0
- package/dist/astro/routes/api/admin/plugins/registry/install.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/plugins/registry/install.mjs +72 -0
- package/dist/astro/routes/api/admin/plugins/registry/install.mjs.map +1 -0
- package/dist/astro/routes/api/admin/plugins/updates.d.mts +8 -0
- package/dist/astro/routes/api/admin/plugins/updates.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/plugins/updates.mjs +49 -0
- package/dist/astro/routes/api/admin/plugins/updates.mjs.map +1 -0
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.d.mts +8 -0
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +51 -0
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs.map +1 -0
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.d.mts +8 -0
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +39 -0
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs.map +1 -0
- package/dist/astro/routes/api/admin/themes/marketplace/index.d.mts +8 -0
- package/dist/astro/routes/api/admin/themes/marketplace/index.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +67 -0
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs.map +1 -0
- package/dist/astro/routes/api/admin/users/_id_/disable.d.mts +8 -0
- package/dist/astro/routes/api/admin/users/_id_/disable.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/users/_id_/disable.mjs +43 -0
- package/dist/astro/routes/api/admin/users/_id_/disable.mjs.map +1 -0
- package/dist/astro/routes/api/admin/users/_id_/enable.d.mts +8 -0
- package/dist/astro/routes/api/admin/users/_id_/enable.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/users/_id_/enable.mjs +32 -0
- package/dist/astro/routes/api/admin/users/_id_/enable.mjs.map +1 -0
- package/dist/astro/routes/api/admin/users/_id_/index.d.mts +9 -0
- package/dist/astro/routes/api/admin/users/_id_/index.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/users/_id_/index.mjs +106 -0
- package/dist/astro/routes/api/admin/users/_id_/index.mjs.map +1 -0
- package/dist/astro/routes/api/admin/users/_id_/send-recovery.d.mts +8 -0
- package/dist/astro/routes/api/admin/users/_id_/send-recovery.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +46 -0
- package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs.map +1 -0
- package/dist/astro/routes/api/admin/users/index.d.mts +8 -0
- package/dist/astro/routes/api/admin/users/index.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/users/index.mjs +56 -0
- package/dist/astro/routes/api/admin/users/index.mjs.map +1 -0
- package/dist/astro/routes/api/auth/dev-bypass.d.mts +9 -0
- package/dist/astro/routes/api/auth/dev-bypass.d.mts.map +1 -0
- package/dist/astro/routes/api/auth/dev-bypass.mjs +84 -0
- package/dist/astro/routes/api/auth/dev-bypass.mjs.map +1 -0
- package/dist/astro/routes/api/auth/invite/accept.d.mts +8 -0
- package/dist/astro/routes/api/auth/invite/accept.d.mts.map +1 -0
- package/dist/astro/routes/api/auth/invite/accept.mjs +34 -0
- package/dist/astro/routes/api/auth/invite/accept.mjs.map +1 -0
- package/dist/astro/routes/api/auth/invite/complete.d.mts +8 -0
- package/dist/astro/routes/api/auth/invite/complete.d.mts.map +1 -0
- package/dist/astro/routes/api/auth/invite/complete.mjs +56 -0
- package/dist/astro/routes/api/auth/invite/complete.mjs.map +1 -0
- package/dist/astro/routes/api/auth/invite/index.d.mts +8 -0
- package/dist/astro/routes/api/auth/invite/index.d.mts.map +1 -0
- package/dist/astro/routes/api/auth/invite/index.mjs +53 -0
- package/dist/astro/routes/api/auth/invite/index.mjs.map +1 -0
- package/dist/astro/routes/api/auth/invite/register-options.d.mts +8 -0
- package/dist/astro/routes/api/auth/invite/register-options.d.mts.map +1 -0
- package/dist/astro/routes/api/auth/invite/register-options.mjs +46 -0
- package/dist/astro/routes/api/auth/invite/register-options.mjs.map +1 -0
- package/dist/astro/routes/api/auth/logout.d.mts +8 -0
- package/dist/astro/routes/api/auth/logout.d.mts.map +1 -0
- package/dist/astro/routes/api/auth/logout.mjs +27 -0
- package/dist/astro/routes/api/auth/logout.mjs.map +1 -0
- package/dist/astro/routes/api/auth/magic-link/send.d.mts +8 -0
- package/dist/astro/routes/api/auth/magic-link/send.d.mts.map +1 -0
- package/dist/astro/routes/api/auth/magic-link/send.mjs +50 -0
- package/dist/astro/routes/api/auth/magic-link/send.mjs.map +1 -0
- package/dist/astro/routes/api/auth/magic-link/verify.d.mts +8 -0
- package/dist/astro/routes/api/auth/magic-link/verify.d.mts.map +1 -0
- package/dist/astro/routes/api/auth/magic-link/verify.mjs +35 -0
- package/dist/astro/routes/api/auth/magic-link/verify.mjs.map +1 -0
- package/dist/astro/routes/api/auth/me.d.mts +14 -0
- package/dist/astro/routes/api/auth/me.d.mts.map +1 -0
- package/dist/astro/routes/api/auth/me.mjs +43 -0
- package/dist/astro/routes/api/auth/me.mjs.map +1 -0
- package/dist/astro/routes/api/auth/mode.d.mts +8 -0
- package/dist/astro/routes/api/auth/mode.d.mts.map +1 -0
- package/dist/astro/routes/api/auth/mode.mjs +29 -0
- package/dist/astro/routes/api/auth/mode.mjs.map +1 -0
- package/dist/astro/routes/api/auth/oauth/_provider_/callback.d.mts +8 -0
- package/dist/astro/routes/api/auth/oauth/_provider_/callback.d.mts.map +1 -0
- package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs +130 -0
- package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs.map +1 -0
- package/dist/astro/routes/api/auth/oauth/_provider_.d.mts +8 -0
- package/dist/astro/routes/api/auth/oauth/_provider_.d.mts.map +1 -0
- package/dist/astro/routes/api/auth/oauth/_provider_.mjs +60 -0
- package/dist/astro/routes/api/auth/oauth/_provider_.mjs.map +1 -0
- package/dist/astro/routes/api/auth/passkey/_id_.d.mts +15 -0
- package/dist/astro/routes/api/auth/passkey/_id_.d.mts.map +1 -0
- package/dist/astro/routes/api/auth/passkey/_id_.mjs +64 -0
- package/dist/astro/routes/api/auth/passkey/_id_.mjs.map +1 -0
- package/dist/astro/routes/api/auth/passkey/index.d.mts +8 -0
- package/dist/astro/routes/api/auth/passkey/index.d.mts.map +1 -0
- package/dist/astro/routes/api/auth/passkey/index.mjs +28 -0
- package/dist/astro/routes/api/auth/passkey/index.mjs.map +1 -0
- package/dist/astro/routes/api/auth/passkey/options.d.mts +8 -0
- package/dist/astro/routes/api/auth/passkey/options.d.mts.map +1 -0
- package/dist/astro/routes/api/auth/passkey/options.mjs +48 -0
- package/dist/astro/routes/api/auth/passkey/options.mjs.map +1 -0
- package/dist/astro/routes/api/auth/passkey/register/options.d.mts +8 -0
- package/dist/astro/routes/api/auth/passkey/register/options.d.mts.map +1 -0
- package/dist/astro/routes/api/auth/passkey/register/options.mjs +46 -0
- package/dist/astro/routes/api/auth/passkey/register/options.mjs.map +1 -0
- package/dist/astro/routes/api/auth/passkey/register/verify.d.mts +8 -0
- package/dist/astro/routes/api/auth/passkey/register/verify.d.mts.map +1 -0
- package/dist/astro/routes/api/auth/passkey/register/verify.mjs +61 -0
- package/dist/astro/routes/api/auth/passkey/register/verify.mjs.map +1 -0
- package/dist/astro/routes/api/auth/passkey/verify.d.mts +8 -0
- package/dist/astro/routes/api/auth/passkey/verify.d.mts.map +1 -0
- package/dist/astro/routes/api/auth/passkey/verify.mjs +49 -0
- package/dist/astro/routes/api/auth/passkey/verify.mjs.map +1 -0
- package/dist/astro/routes/api/auth/signup/complete.d.mts +8 -0
- package/dist/astro/routes/api/auth/signup/complete.d.mts.map +1 -0
- package/dist/astro/routes/api/auth/signup/complete.mjs +57 -0
- package/dist/astro/routes/api/auth/signup/complete.mjs.map +1 -0
- package/dist/astro/routes/api/auth/signup/request.d.mts +8 -0
- package/dist/astro/routes/api/auth/signup/request.d.mts.map +1 -0
- package/dist/astro/routes/api/auth/signup/request.mjs +46 -0
- package/dist/astro/routes/api/auth/signup/request.mjs.map +1 -0
- package/dist/astro/routes/api/auth/signup/verify.d.mts +8 -0
- package/dist/astro/routes/api/auth/signup/verify.d.mts.map +1 -0
- package/dist/astro/routes/api/auth/signup/verify.mjs +35 -0
- package/dist/astro/routes/api/auth/signup/verify.mjs.map +1 -0
- package/dist/astro/routes/api/comments/_collection_/_contentId_/index.d.mts +15 -0
- package/dist/astro/routes/api/comments/_collection_/_contentId_/index.d.mts.map +1 -0
- package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +193 -0
- package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs.map +1 -0
- package/dist/astro/routes/api/content/_collection_/_id_/compare.d.mts +8 -0
- package/dist/astro/routes/api/content/_collection_/_id_/compare.d.mts.map +1 -0
- package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +20 -0
- package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs.map +1 -0
- package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.d.mts +8 -0
- package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.d.mts.map +1 -0
- package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +28 -0
- package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs.map +1 -0
- package/dist/astro/routes/api/content/_collection_/_id_/duplicate.d.mts +8 -0
- package/dist/astro/routes/api/content/_collection_/_id_/duplicate.d.mts.map +1 -0
- package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +30 -0
- package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs.map +1 -0
- package/dist/astro/routes/api/content/_collection_/_id_/permanent.d.mts +8 -0
- package/dist/astro/routes/api/content/_collection_/_id_/permanent.d.mts.map +1 -0
- package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +23 -0
- package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs.map +1 -0
- package/dist/astro/routes/api/content/_collection_/_id_/preview-url.d.mts +8 -0
- package/dist/astro/routes/api/content/_collection_/_id_/preview-url.d.mts.map +1 -0
- package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +78 -0
- package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs.map +1 -0
- package/dist/astro/routes/api/content/_collection_/_id_/publish.d.mts +8 -0
- package/dist/astro/routes/api/content/_collection_/_id_/publish.d.mts.map +1 -0
- package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +48 -0
- package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs.map +1 -0
- package/dist/astro/routes/api/content/_collection_/_id_/restore.d.mts +8 -0
- package/dist/astro/routes/api/content/_collection_/_id_/restore.d.mts.map +1 -0
- package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +28 -0
- package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs.map +1 -0
- package/dist/astro/routes/api/content/_collection_/_id_/revisions.d.mts +8 -0
- package/dist/astro/routes/api/content/_collection_/_id_/revisions.d.mts.map +1 -0
- package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +22 -0
- package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs.map +1 -0
- package/dist/astro/routes/api/content/_collection_/_id_/schedule.d.mts +9 -0
- package/dist/astro/routes/api/content/_collection_/_id_/schedule.d.mts.map +1 -0
- package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +58 -0
- package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs.map +1 -0
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.d.mts +15 -0
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.d.mts.map +1 -0
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +85 -0
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs.map +1 -0
- package/dist/astro/routes/api/content/_collection_/_id_/translations.d.mts +8 -0
- package/dist/astro/routes/api/content/_collection_/_id_/translations.d.mts.map +1 -0
- package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +43 -0
- package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs.map +1 -0
- package/dist/astro/routes/api/content/_collection_/_id_/unpublish.d.mts +8 -0
- package/dist/astro/routes/api/content/_collection_/_id_/unpublish.d.mts.map +1 -0
- package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +28 -0
- package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs.map +1 -0
- package/dist/astro/routes/api/content/_collection_/_id_.d.mts +10 -0
- package/dist/astro/routes/api/content/_collection_/_id_.d.mts.map +1 -0
- package/dist/astro/routes/api/content/_collection_/_id_.mjs +88 -0
- package/dist/astro/routes/api/content/_collection_/_id_.mjs.map +1 -0
- package/dist/astro/routes/api/content/_collection_/index.d.mts +9 -0
- package/dist/astro/routes/api/content/_collection_/index.d.mts.map +1 -0
- package/dist/astro/routes/api/content/_collection_/index.mjs +61 -0
- package/dist/astro/routes/api/content/_collection_/index.mjs.map +1 -0
- package/dist/astro/routes/api/content/_collection_/trash.d.mts +8 -0
- package/dist/astro/routes/api/content/_collection_/trash.d.mts.map +1 -0
- package/dist/astro/routes/api/content/_collection_/trash.mjs +25 -0
- package/dist/astro/routes/api/content/_collection_/trash.mjs.map +1 -0
- package/dist/astro/routes/api/dashboard.d.mts +8 -0
- package/dist/astro/routes/api/dashboard.d.mts.map +1 -0
- package/dist/astro/routes/api/dashboard.mjs +26 -0
- package/dist/astro/routes/api/dashboard.mjs.map +1 -0
- package/dist/astro/routes/api/dev/emails.d.mts +9 -0
- package/dist/astro/routes/api/dev/emails.d.mts.map +1 -0
- package/dist/astro/routes/api/dev/emails.mjs +20 -0
- package/dist/astro/routes/api/dev/emails.mjs.map +1 -0
- package/dist/astro/routes/api/import/probe.d.mts +18 -0
- package/dist/astro/routes/api/import/probe.d.mts.map +1 -0
- package/dist/astro/routes/api/import/probe.mjs +35 -0
- package/dist/astro/routes/api/import/probe.mjs.map +1 -0
- package/dist/astro/routes/api/import/wordpress/analyze.d.mts +88 -0
- package/dist/astro/routes/api/import/wordpress/analyze.d.mts.map +1 -0
- package/dist/astro/routes/api/import/wordpress/analyze.mjs +313 -0
- package/dist/astro/routes/api/import/wordpress/analyze.mjs.map +1 -0
- package/dist/astro/routes/api/import/wordpress/execute.d.mts +93 -0
- package/dist/astro/routes/api/import/wordpress/execute.d.mts.map +1 -0
- package/dist/astro/routes/api/import/wordpress/execute.mjs +593 -0
- package/dist/astro/routes/api/import/wordpress/execute.mjs.map +1 -0
- package/dist/astro/routes/api/import/wordpress/media.d.mts +36 -0
- package/dist/astro/routes/api/import/wordpress/media.d.mts.map +1 -0
- package/dist/astro/routes/api/import/wordpress/media.mjs +225 -0
- package/dist/astro/routes/api/import/wordpress/media.mjs.map +1 -0
- package/dist/astro/routes/api/import/wordpress/prepare.d.mts +20 -0
- package/dist/astro/routes/api/import/wordpress/prepare.d.mts.map +1 -0
- package/dist/astro/routes/api/import/wordpress/prepare.mjs +120 -0
- package/dist/astro/routes/api/import/wordpress/prepare.mjs.map +1 -0
- package/dist/astro/routes/api/import/wordpress/rewrite-url-helpers.d.mts +49 -0
- package/dist/astro/routes/api/import/wordpress/rewrite-url-helpers.d.mts.map +1 -0
- package/dist/astro/routes/api/import/wordpress/rewrite-url-helpers.mjs +131 -0
- package/dist/astro/routes/api/import/wordpress/rewrite-url-helpers.mjs.map +1 -0
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts +22 -0
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts.map +1 -0
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +139 -0
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs.map +1 -0
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.d.mts +16 -0
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.d.mts.map +1 -0
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +71 -0
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs.map +1 -0
- package/dist/astro/routes/api/import/wordpress-plugin/callback.d.mts +8 -0
- package/dist/astro/routes/api/import/wordpress-plugin/callback.d.mts.map +1 -0
- package/dist/astro/routes/api/import/wordpress-plugin/callback.mjs +29 -0
- package/dist/astro/routes/api/import/wordpress-plugin/callback.mjs.map +1 -0
- package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +20 -0
- package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts.map +1 -0
- package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +219 -0
- package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs.map +1 -0
- package/dist/astro/routes/api/manifest.d.mts +8 -0
- package/dist/astro/routes/api/manifest.d.mts.map +1 -0
- package/dist/astro/routes/api/manifest.mjs +47 -0
- package/dist/astro/routes/api/manifest.mjs.map +1 -0
- package/dist/astro/routes/api/mcp.d.mts +16 -0
- package/dist/astro/routes/api/mcp.d.mts.map +1 -0
- package/dist/astro/routes/api/mcp.mjs +1414 -0
- package/dist/astro/routes/api/mcp.mjs.map +1 -0
- package/dist/astro/routes/api/media/_id_/confirm.d.mts +11 -0
- package/dist/astro/routes/api/media/_id_/confirm.d.mts.map +1 -0
- package/dist/astro/routes/api/media/_id_/confirm.mjs +61 -0
- package/dist/astro/routes/api/media/_id_/confirm.mjs.map +1 -0
- package/dist/astro/routes/api/media/_id_.d.mts +23 -0
- package/dist/astro/routes/api/media/_id_.d.mts.map +1 -0
- package/dist/astro/routes/api/media/_id_.mjs +83 -0
- package/dist/astro/routes/api/media/_id_.mjs.map +1 -0
- package/dist/astro/routes/api/media/file/_...key_.d.mts +8 -0
- package/dist/astro/routes/api/media/file/_...key_.d.mts.map +1 -0
- package/dist/astro/routes/api/media/file/_...key_.mjs +52 -0
- package/dist/astro/routes/api/media/file/_...key_.mjs.map +1 -0
- package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.d.mts +15 -0
- package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.d.mts.map +1 -0
- package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +52 -0
- package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs.map +1 -0
- package/dist/astro/routes/api/media/providers/_providerId_/index.d.mts +15 -0
- package/dist/astro/routes/api/media/providers/_providerId_/index.d.mts.map +1 -0
- package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +75 -0
- package/dist/astro/routes/api/media/providers/_providerId_/index.mjs.map +1 -0
- package/dist/astro/routes/api/media/providers/index.d.mts +11 -0
- package/dist/astro/routes/api/media/providers/index.d.mts.map +1 -0
- package/dist/astro/routes/api/media/providers/index.mjs +21 -0
- package/dist/astro/routes/api/media/providers/index.mjs.map +1 -0
- package/dist/astro/routes/api/media/upload-url.d.mts +11 -0
- package/dist/astro/routes/api/media/upload-url.d.mts.map +1 -0
- package/dist/astro/routes/api/media/upload-url.mjs +82 -0
- package/dist/astro/routes/api/media/upload-url.mjs.map +1 -0
- package/dist/astro/routes/api/media.d.mts +17 -0
- package/dist/astro/routes/api/media.d.mts.map +1 -0
- package/dist/astro/routes/api/media.mjs +138 -0
- package/dist/astro/routes/api/media.mjs.map +1 -0
- package/dist/astro/routes/api/menus/_name_/items/_id_.d.mts +9 -0
- package/dist/astro/routes/api/menus/_name_/items/_id_.d.mts.map +1 -0
- package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +48 -0
- package/dist/astro/routes/api/menus/_name_/items/_id_.mjs.map +1 -0
- package/dist/astro/routes/api/menus/_name_/items.d.mts +8 -0
- package/dist/astro/routes/api/menus/_name_/items.d.mts.map +1 -0
- package/dist/astro/routes/api/menus/_name_/items.mjs +31 -0
- package/dist/astro/routes/api/menus/_name_/items.mjs.map +1 -0
- package/dist/astro/routes/api/menus/_name_/reorder.d.mts +8 -0
- package/dist/astro/routes/api/menus/_name_/reorder.d.mts.map +1 -0
- package/dist/astro/routes/api/menus/_name_/reorder.mjs +31 -0
- package/dist/astro/routes/api/menus/_name_/reorder.mjs.map +1 -0
- package/dist/astro/routes/api/menus/_name_/translations.d.mts +9 -0
- package/dist/astro/routes/api/menus/_name_/translations.d.mts.map +1 -0
- package/dist/astro/routes/api/menus/_name_/translations.mjs +62 -0
- package/dist/astro/routes/api/menus/_name_/translations.mjs.map +1 -0
- package/dist/astro/routes/api/menus/_name_.d.mts +10 -0
- package/dist/astro/routes/api/menus/_name_.d.mts.map +1 -0
- package/dist/astro/routes/api/menus/_name_.mjs +60 -0
- package/dist/astro/routes/api/menus/_name_.mjs.map +1 -0
- package/dist/astro/routes/api/menus/index.d.mts +9 -0
- package/dist/astro/routes/api/menus/index.d.mts.map +1 -0
- package/dist/astro/routes/api/menus/index.mjs +40 -0
- package/dist/astro/routes/api/menus/index.mjs.map +1 -0
- package/dist/astro/routes/api/oauth/authorize.d.mts +9 -0
- package/dist/astro/routes/api/oauth/authorize.d.mts.map +1 -0
- package/dist/astro/routes/api/oauth/authorize.mjs +260 -0
- package/dist/astro/routes/api/oauth/authorize.mjs.map +1 -0
- package/dist/astro/routes/api/oauth/device/authorize.d.mts +8 -0
- package/dist/astro/routes/api/oauth/device/authorize.d.mts.map +1 -0
- package/dist/astro/routes/api/oauth/device/authorize.mjs +32 -0
- package/dist/astro/routes/api/oauth/device/authorize.mjs.map +1 -0
- package/dist/astro/routes/api/oauth/device/code.d.mts +8 -0
- package/dist/astro/routes/api/oauth/device/code.d.mts.map +1 -0
- package/dist/astro/routes/api/oauth/device/code.mjs +36 -0
- package/dist/astro/routes/api/oauth/device/code.mjs.map +1 -0
- package/dist/astro/routes/api/oauth/device/token.d.mts +8 -0
- package/dist/astro/routes/api/oauth/device/token.d.mts.map +1 -0
- package/dist/astro/routes/api/oauth/device/token.mjs +47 -0
- package/dist/astro/routes/api/oauth/device/token.mjs.map +1 -0
- package/dist/astro/routes/api/oauth/register.d.mts +9 -0
- package/dist/astro/routes/api/oauth/register.d.mts.map +1 -0
- package/dist/astro/routes/api/oauth/register.mjs +113 -0
- package/dist/astro/routes/api/oauth/register.mjs.map +1 -0
- package/dist/astro/routes/api/oauth/token/refresh.d.mts +8 -0
- package/dist/astro/routes/api/oauth/token/refresh.d.mts.map +1 -0
- package/dist/astro/routes/api/oauth/token/refresh.mjs +30 -0
- package/dist/astro/routes/api/oauth/token/refresh.mjs.map +1 -0
- package/dist/astro/routes/api/oauth/token/revoke.d.mts +8 -0
- package/dist/astro/routes/api/oauth/token/revoke.d.mts.map +1 -0
- package/dist/astro/routes/api/oauth/token/revoke.mjs +27 -0
- package/dist/astro/routes/api/oauth/token/revoke.mjs.map +1 -0
- package/dist/astro/routes/api/oauth/token.d.mts +9 -0
- package/dist/astro/routes/api/oauth/token.d.mts.map +1 -0
- package/dist/astro/routes/api/oauth/token.mjs +141 -0
- package/dist/astro/routes/api/oauth/token.mjs.map +1 -0
- package/dist/astro/routes/api/openapi.json.d.mts +8 -0
- package/dist/astro/routes/api/openapi.json.d.mts.map +1 -0
- package/dist/astro/routes/api/openapi.json.mjs +2642 -0
- package/dist/astro/routes/api/openapi.json.mjs.map +1 -0
- package/dist/astro/routes/api/plugins/_pluginId_/_...path_.d.mts +12 -0
- package/dist/astro/routes/api/plugins/_pluginId_/_...path_.d.mts.map +1 -0
- package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +78 -0
- package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs.map +1 -0
- package/dist/astro/routes/api/redirects/404s/index.d.mts +10 -0
- package/dist/astro/routes/api/redirects/404s/index.d.mts.map +1 -0
- package/dist/astro/routes/api/redirects/404s/index.mjs +62 -0
- package/dist/astro/routes/api/redirects/404s/index.mjs.map +1 -0
- package/dist/astro/routes/api/redirects/404s/summary.d.mts +8 -0
- package/dist/astro/routes/api/redirects/404s/summary.d.mts.map +1 -0
- package/dist/astro/routes/api/redirects/404s/summary.mjs +34 -0
- package/dist/astro/routes/api/redirects/404s/summary.mjs.map +1 -0
- package/dist/astro/routes/api/redirects/_id_.d.mts +10 -0
- package/dist/astro/routes/api/redirects/_id_.d.mts.map +1 -0
- package/dist/astro/routes/api/redirects/_id_.mjs +71 -0
- package/dist/astro/routes/api/redirects/_id_.mjs.map +1 -0
- package/dist/astro/routes/api/redirects/index.d.mts +9 -0
- package/dist/astro/routes/api/redirects/index.d.mts.map +1 -0
- package/dist/astro/routes/api/redirects/index.mjs +52 -0
- package/dist/astro/routes/api/redirects/index.mjs.map +1 -0
- package/dist/astro/routes/api/revisions/_revisionId_/index.d.mts +8 -0
- package/dist/astro/routes/api/revisions/_revisionId_/index.d.mts.map +1 -0
- package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +19 -0
- package/dist/astro/routes/api/revisions/_revisionId_/index.mjs.map +1 -0
- package/dist/astro/routes/api/revisions/_revisionId_/restore.d.mts +8 -0
- package/dist/astro/routes/api/revisions/_revisionId_/restore.d.mts.map +1 -0
- package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +26 -0
- package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs.map +1 -0
- package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.d.mts +10 -0
- package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.d.mts.map +1 -0
- package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +75 -0
- package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs.map +1 -0
- package/dist/astro/routes/api/schema/collections/_slug_/fields/index.d.mts +9 -0
- package/dist/astro/routes/api/schema/collections/_slug_/fields/index.d.mts.map +1 -0
- package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +63 -0
- package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs.map +1 -0
- package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.d.mts +8 -0
- package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.d.mts.map +1 -0
- package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +54 -0
- package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs.map +1 -0
- package/dist/astro/routes/api/schema/collections/_slug_/index.d.mts +10 -0
- package/dist/astro/routes/api/schema/collections/_slug_/index.d.mts.map +1 -0
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +79 -0
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs.map +1 -0
- package/dist/astro/routes/api/schema/collections/index.d.mts +9 -0
- package/dist/astro/routes/api/schema/collections/index.d.mts.map +1 -0
- package/dist/astro/routes/api/schema/collections/index.mjs +63 -0
- package/dist/astro/routes/api/schema/collections/index.mjs.map +1 -0
- package/dist/astro/routes/api/schema/index.d.mts +8 -0
- package/dist/astro/routes/api/schema/index.d.mts.map +1 -0
- package/dist/astro/routes/api/schema/index.mjs +82 -0
- package/dist/astro/routes/api/schema/index.mjs.map +1 -0
- package/dist/astro/routes/api/schema/orphans/_slug_.d.mts +8 -0
- package/dist/astro/routes/api/schema/orphans/_slug_.d.mts.map +1 -0
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs +55 -0
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs.map +1 -0
- package/dist/astro/routes/api/schema/orphans/index.d.mts +8 -0
- package/dist/astro/routes/api/schema/orphans/index.d.mts.map +1 -0
- package/dist/astro/routes/api/schema/orphans/index.mjs +50 -0
- package/dist/astro/routes/api/schema/orphans/index.mjs.map +1 -0
- package/dist/astro/routes/api/search/enable.d.mts +16 -0
- package/dist/astro/routes/api/search/enable.d.mts.map +1 -0
- package/dist/astro/routes/api/search/enable.mjs +55 -0
- package/dist/astro/routes/api/search/enable.mjs.map +1 -0
- package/dist/astro/routes/api/search/index.d.mts +17 -0
- package/dist/astro/routes/api/search/index.d.mts.map +1 -0
- package/dist/astro/routes/api/search/index.mjs +52 -0
- package/dist/astro/routes/api/search/index.mjs.map +1 -0
- package/dist/astro/routes/api/search/rebuild.d.mts +14 -0
- package/dist/astro/routes/api/search/rebuild.d.mts.map +1 -0
- package/dist/astro/routes/api/search/rebuild.mjs +48 -0
- package/dist/astro/routes/api/search/rebuild.mjs.map +1 -0
- package/dist/astro/routes/api/search/stats.d.mts +11 -0
- package/dist/astro/routes/api/search/stats.d.mts.map +1 -0
- package/dist/astro/routes/api/search/stats.mjs +29 -0
- package/dist/astro/routes/api/search/stats.mjs.map +1 -0
- package/dist/astro/routes/api/search/suggest.d.mts +16 -0
- package/dist/astro/routes/api/search/suggest.d.mts.map +1 -0
- package/dist/astro/routes/api/search/suggest.mjs +43 -0
- package/dist/astro/routes/api/search/suggest.mjs.map +1 -0
- package/dist/astro/routes/api/sections/_slug_.d.mts +10 -0
- package/dist/astro/routes/api/sections/_slug_.d.mts.map +1 -0
- package/dist/astro/routes/api/sections/_slug_.mjs +65 -0
- package/dist/astro/routes/api/sections/_slug_.mjs.map +1 -0
- package/dist/astro/routes/api/sections/index.d.mts +9 -0
- package/dist/astro/routes/api/sections/index.d.mts.map +1 -0
- package/dist/astro/routes/api/sections/index.mjs +48 -0
- package/dist/astro/routes/api/sections/index.mjs.map +1 -0
- package/dist/astro/routes/api/settings/email.d.mts +18 -0
- package/dist/astro/routes/api/settings/email.d.mts.map +1 -0
- package/dist/astro/routes/api/settings/email.mjs +105 -0
- package/dist/astro/routes/api/settings/email.mjs.map +1 -0
- package/dist/astro/routes/api/settings.d.mts +21 -0
- package/dist/astro/routes/api/settings.d.mts.map +1 -0
- package/dist/astro/routes/api/settings.mjs +58 -0
- package/dist/astro/routes/api/settings.mjs.map +1 -0
- package/dist/astro/routes/api/setup/admin-verify.d.mts +8 -0
- package/dist/astro/routes/api/setup/admin-verify.d.mts.map +1 -0
- package/dist/astro/routes/api/setup/admin-verify.mjs +68 -0
- package/dist/astro/routes/api/setup/admin-verify.mjs.map +1 -0
- package/dist/astro/routes/api/setup/admin.d.mts +8 -0
- package/dist/astro/routes/api/setup/admin.d.mts.map +1 -0
- package/dist/astro/routes/api/setup/admin.mjs +69 -0
- package/dist/astro/routes/api/setup/admin.mjs.map +1 -0
- package/dist/astro/routes/api/setup/dev-bypass.d.mts +9 -0
- package/dist/astro/routes/api/setup/dev-bypass.d.mts.map +1 -0
- package/dist/astro/routes/api/setup/dev-bypass.mjs +139 -0
- package/dist/astro/routes/api/setup/dev-bypass.mjs.map +1 -0
- package/dist/astro/routes/api/setup/dev-reset.d.mts +8 -0
- package/dist/astro/routes/api/setup/dev-reset.d.mts.map +1 -0
- package/dist/astro/routes/api/setup/dev-reset.mjs +25 -0
- package/dist/astro/routes/api/setup/dev-reset.mjs.map +1 -0
- package/dist/astro/routes/api/setup/index.d.mts +8 -0
- package/dist/astro/routes/api/setup/index.d.mts.map +1 -0
- package/dist/astro/routes/api/setup/index.mjs +93 -0
- package/dist/astro/routes/api/setup/index.mjs.map +1 -0
- package/dist/astro/routes/api/setup/status.d.mts +8 -0
- package/dist/astro/routes/api/setup/status.d.mts.map +1 -0
- package/dist/astro/routes/api/setup/status.mjs +60 -0
- package/dist/astro/routes/api/setup/status.mjs.map +1 -0
- package/dist/astro/routes/api/snapshot.d.mts +8 -0
- package/dist/astro/routes/api/snapshot.d.mts.map +1 -0
- package/dist/astro/routes/api/snapshot.mjs +270 -0
- package/dist/astro/routes/api/snapshot.mjs.map +1 -0
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.d.mts +9 -0
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.d.mts.map +1 -0
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +72 -0
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs.map +1 -0
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.d.mts +19 -0
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.d.mts.map +1 -0
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +80 -0
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs.map +1 -0
- package/dist/astro/routes/api/taxonomies/_name_/terms/index.d.mts +15 -0
- package/dist/astro/routes/api/taxonomies/_name_/terms/index.d.mts.map +1 -0
- package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +59 -0
- package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs.map +1 -0
- package/dist/astro/routes/api/taxonomies/index.d.mts +15 -0
- package/dist/astro/routes/api/taxonomies/index.d.mts.map +1 -0
- package/dist/astro/routes/api/taxonomies/index.mjs +55 -0
- package/dist/astro/routes/api/taxonomies/index.mjs.map +1 -0
- package/dist/astro/routes/api/themes/preview.d.mts +8 -0
- package/dist/astro/routes/api/themes/preview.d.mts.map +1 -0
- package/dist/astro/routes/api/themes/preview.mjs +49 -0
- package/dist/astro/routes/api/themes/preview.mjs.map +1 -0
- package/dist/astro/routes/api/typegen.d.mts +18 -0
- package/dist/astro/routes/api/typegen.d.mts.map +1 -0
- package/dist/astro/routes/api/typegen.mjs +78 -0
- package/dist/astro/routes/api/typegen.mjs.map +1 -0
- package/dist/astro/routes/api/well-known/auth.d.mts +8 -0
- package/dist/astro/routes/api/well-known/auth.d.mts.map +1 -0
- package/dist/astro/routes/api/well-known/auth.mjs +42 -0
- package/dist/astro/routes/api/well-known/auth.mjs.map +1 -0
- package/dist/astro/routes/api/well-known/oauth-authorization-server.d.mts +8 -0
- package/dist/astro/routes/api/well-known/oauth-authorization-server.d.mts.map +1 -0
- package/dist/astro/routes/api/well-known/oauth-authorization-server.mjs +32 -0
- package/dist/astro/routes/api/well-known/oauth-authorization-server.mjs.map +1 -0
- package/dist/astro/routes/api/well-known/oauth-protected-resource.d.mts +8 -0
- package/dist/astro/routes/api/well-known/oauth-protected-resource.d.mts.map +1 -0
- package/dist/astro/routes/api/well-known/oauth-protected-resource.mjs +21 -0
- package/dist/astro/routes/api/well-known/oauth-protected-resource.mjs.map +1 -0
- package/dist/astro/routes/api/widget-areas/_name_/reorder.d.mts +8 -0
- package/dist/astro/routes/api/widget-areas/_name_/reorder.d.mts.map +1 -0
- package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +36 -0
- package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs.map +1 -0
- package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.d.mts +9 -0
- package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.d.mts.map +1 -0
- package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +62 -0
- package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs.map +1 -0
- package/dist/astro/routes/api/widget-areas/_name_/widgets.d.mts +8 -0
- package/dist/astro/routes/api/widget-areas/_name_/widgets.d.mts.map +1 -0
- package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +49 -0
- package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs.map +1 -0
- package/dist/astro/routes/api/widget-areas/_name_.d.mts +9 -0
- package/dist/astro/routes/api/widget-areas/_name_.d.mts.map +1 -0
- package/dist/astro/routes/api/widget-areas/_name_.mjs +49 -0
- package/dist/astro/routes/api/widget-areas/_name_.mjs.map +1 -0
- package/dist/astro/routes/api/widget-areas/index.d.mts +9 -0
- package/dist/astro/routes/api/widget-areas/index.d.mts.map +1 -0
- package/dist/astro/routes/api/widget-areas/index.mjs +59 -0
- package/dist/astro/routes/api/widget-areas/index.mjs.map +1 -0
- package/dist/astro/routes/api/widget-components.d.mts +8 -0
- package/dist/astro/routes/api/widget-components.d.mts.map +1 -0
- package/dist/astro/routes/api/widget-components.mjs +18 -0
- package/dist/astro/routes/api/widget-components.mjs.map +1 -0
- package/dist/astro/routes/robots.txt.d.mts +8 -0
- package/dist/astro/routes/robots.txt.d.mts.map +1 -0
- package/dist/astro/routes/robots.txt.mjs +61 -0
- package/dist/astro/routes/robots.txt.mjs.map +1 -0
- package/dist/astro/routes/sitemap-_collection_.xml.d.mts +8 -0
- package/dist/astro/routes/sitemap-_collection_.xml.d.mts.map +1 -0
- package/dist/astro/routes/sitemap-_collection_.xml.mjs +71 -0
- package/dist/astro/routes/sitemap-_collection_.xml.mjs.map +1 -0
- package/dist/astro/routes/sitemap.xml.d.mts +8 -0
- package/dist/astro/routes/sitemap.xml.d.mts.map +1 -0
- package/dist/astro/routes/sitemap.xml.mjs +64 -0
- package/dist/astro/routes/sitemap.xml.mjs.map +1 -0
- package/dist/astro/types.d.mts +48 -8
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/auth/providers/github.d.mts +13 -0
- package/dist/auth/providers/github.d.mts.map +1 -0
- package/dist/auth/providers/github.mjs +18 -0
- package/dist/auth/providers/github.mjs.map +1 -0
- package/dist/auth/providers/google.d.mts +13 -0
- package/dist/auth/providers/google.d.mts.map +1 -0
- package/dist/auth/providers/google.mjs +18 -0
- package/dist/auth/providers/google.mjs.map +1 -0
- package/dist/authorize-BlyCH-96.mjs +37 -0
- package/dist/authorize-BlyCH-96.mjs.map +1 -0
- package/dist/{base64-MBPo9ozB.mjs → base64-CqR-7kqF.mjs} +1 -1
- package/dist/{base64-MBPo9ozB.mjs.map → base64-CqR-7kqF.mjs.map} +1 -1
- package/dist/{byline-gFn1r0vA.mjs → byline-D09BaS4j.mjs} +4 -4
- package/dist/{byline-gFn1r0vA.mjs.map → byline-D09BaS4j.mjs.map} +1 -1
- package/dist/{bylines-DTFI8nDM.mjs → bylines-BTM2xtP8.mjs} +6 -6
- package/dist/{bylines-DTFI8nDM.mjs.map → bylines-BTM2xtP8.mjs.map} +1 -1
- package/dist/bylines-C6eYUWlZ.d.mts +1971 -0
- package/dist/bylines-C6eYUWlZ.d.mts.map +1 -0
- package/dist/{cache-BAJbeoZ8.mjs → cache-CXCpjWiL.mjs} +3 -3
- package/dist/{cache-BAJbeoZ8.mjs.map → cache-CXCpjWiL.mjs.map} +1 -1
- package/dist/challenge-store-CJ0OOHOr.mjs +49 -0
- package/dist/challenge-store-CJ0OOHOr.mjs.map +1 -0
- package/dist/{chunks-BK1oZS-l.mjs → chunks-DyGtu1Bv.mjs} +2 -2
- package/dist/{chunks-BK1oZS-l.mjs.map → chunks-DyGtu1Bv.mjs.map} +1 -1
- package/dist/cli/index.mjs +23 -18
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/cf-access.d.mts +1 -1
- package/dist/client/index.d.mts +1 -1
- package/dist/client/index.d.mts.map +1 -1
- package/dist/client/index.mjs +2 -2
- package/dist/client/index.mjs.map +1 -1
- package/dist/comment-Dd9MI82-.mjs +247 -0
- package/dist/comment-Dd9MI82-.mjs.map +1 -0
- package/dist/comments-koGI0FrK.mjs +204 -0
- package/dist/comments-koGI0FrK.mjs.map +1 -0
- package/dist/components-mZem7pbe.mjs +108 -0
- package/dist/components-mZem7pbe.mjs.map +1 -0
- package/dist/{content-CERxPUN0.mjs → content-D6YG26WG.mjs} +10 -34
- package/dist/content-D6YG26WG.mjs.map +1 -0
- package/dist/context-qF8d3IPR.mjs +879 -0
- package/dist/context-qF8d3IPR.mjs.map +1 -0
- package/dist/cron-H8eJ46dv.mjs +264 -0
- package/dist/cron-H8eJ46dv.mjs.map +1 -0
- package/dist/dashboard-BmWSIUwY.mjs +105 -0
- package/dist/dashboard-BmWSIUwY.mjs.map +1 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +1 -1
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/{db-errors-B7P2pSCn.mjs → db-errors-CGN9kJfo.mjs} +1 -1
- package/dist/{db-errors-B7P2pSCn.mjs.map → db-errors-CGN9kJfo.mjs.map} +1 -1
- package/dist/{default-pHuz9WF6.mjs → default-Dbs22Gg4.mjs} +1 -1
- package/dist/{default-pHuz9WF6.mjs.map → default-Dbs22Gg4.mjs.map} +1 -1
- package/dist/device-flow-BqJRxa0Q.mjs +467 -0
- package/dist/device-flow-BqJRxa0Q.mjs.map +1 -0
- package/dist/email-console-Dmp5Q-P2.mjs +50 -0
- package/dist/email-console-Dmp5Q-P2.mjs.map +1 -0
- package/dist/error-tSQWIl5U.mjs +437 -0
- package/dist/error-tSQWIl5U.mjs.map +1 -0
- package/dist/escape-B8bdIryO.mjs +9 -0
- package/dist/escape-B8bdIryO.mjs.map +1 -0
- package/dist/fts-manager-B633C-kQ.mjs +339 -0
- package/dist/fts-manager-B633C-kQ.mjs.map +1 -0
- package/dist/hash-DlUxGhQS.mjs +33 -0
- package/dist/hash-DlUxGhQS.mjs.map +1 -0
- package/dist/import-CNfLOgDE.mjs +1531 -0
- package/dist/import-CNfLOgDE.mjs.map +1 -0
- package/dist/index-D2gvztOP.d.mts +262 -0
- package/dist/index-D2gvztOP.d.mts.map +1 -0
- package/dist/{index-Dlkzhb4C.d.mts → index-UmOMt9T-.d.mts} +310 -911
- package/dist/index-UmOMt9T-.d.mts.map +1 -0
- package/dist/index.d.mts +17 -11
- package/dist/index.mjs +57 -28
- package/dist/{load-DR1VwFXR.mjs → load-QzYRpVN3.mjs} +2 -2
- package/dist/{load-DR1VwFXR.mjs.map → load-QzYRpVN3.mjs.map} +1 -1
- package/dist/{loader-ou_PXAjg.mjs → loader-Cs6-Bqe6.mjs} +4 -4
- package/dist/{loader-ou_PXAjg.mjs.map → loader-Cs6-Bqe6.mjs.map} +1 -1
- package/dist/{manifest-schema-Bp6d4d4n.mjs → manifest-schema-HCtSh4Jq.mjs} +1 -1
- package/dist/{manifest-schema-Bp6d4d4n.mjs.map → manifest-schema-HCtSh4Jq.mjs.map} +1 -1
- package/dist/media/index.d.mts +1 -1
- package/dist/media/index.mjs +2 -1
- package/dist/media/index.mjs.map +1 -1
- package/dist/media/local-runtime.d.mts +11 -7
- package/dist/media/local-runtime.d.mts.map +1 -1
- package/dist/media/local-runtime.mjs +7 -6
- package/dist/media/local-runtime.mjs.map +1 -1
- package/dist/media-Dg7he9uK.mjs +209 -0
- package/dist/media-Dg7he9uK.mjs.map +1 -0
- package/dist/media-allowlist-B8EX01DH.mjs +32 -0
- package/dist/media-allowlist-B8EX01DH.mjs.map +1 -0
- package/dist/menus-DOzIecHi.mjs +723 -0
- package/dist/menus-DOzIecHi.mjs.map +1 -0
- package/dist/menus-X4Z-eBA1.mjs +2788 -0
- package/dist/menus-X4Z-eBA1.mjs.map +1 -0
- package/dist/mime-KV5TqkMN.mjs +36 -0
- package/dist/mime-KV5TqkMN.mjs.map +1 -0
- package/dist/{mode-YhqNVef_.mjs → mode-DPRPvJYm.mjs} +1 -1
- package/dist/{mode-YhqNVef_.mjs.map → mode-DPRPvJYm.mjs.map} +1 -1
- package/dist/normalize-CN5kRSMC.mjs +151 -0
- package/dist/normalize-CN5kRSMC.mjs.map +1 -0
- package/dist/oauth-authorization-62GmpGIH.mjs +275 -0
- package/dist/oauth-authorization-62GmpGIH.mjs.map +1 -0
- package/dist/oauth-clients-D_B0_-Bz.mjs +266 -0
- package/dist/oauth-clients-D_B0_-Bz.mjs.map +1 -0
- package/dist/oauth-state-store-DpsZViTu.mjs +49 -0
- package/dist/oauth-state-store-DpsZViTu.mjs.map +1 -0
- package/dist/oauth-user-lookup-meyS2oB1.mjs +26 -0
- package/dist/oauth-user-lookup-meyS2oB1.mjs.map +1 -0
- package/dist/{options-nPxWnrya.mjs → options-BL4X94qY.mjs} +1 -1
- package/dist/{options-nPxWnrya.mjs.map → options-BL4X94qY.mjs.map} +1 -1
- package/dist/options-Cq64Wx0O.d.mts +207 -0
- package/dist/options-Cq64Wx0O.d.mts.map +1 -0
- package/dist/page/index.d.mts +2 -2
- package/dist/parse-BFTPon-J.mjs +89 -0
- package/dist/parse-BFTPon-J.mjs.map +1 -0
- package/dist/passkey-config-Cg86_ISa.mjs +46 -0
- package/dist/passkey-config-Cg86_ISa.mjs.map +1 -0
- package/dist/{patterns-DsUZ4uxI.mjs → patterns-CqG5Ya3i.mjs} +54 -2
- package/dist/{patterns-DsUZ4uxI.mjs.map → patterns-CqG5Ya3i.mjs.map} +1 -1
- package/dist/{placeholder-CDPtkelt.d.mts → placeholder-D3cFCU9y.d.mts} +2 -1
- package/dist/{placeholder-CDPtkelt.d.mts.map → placeholder-D3cFCU9y.d.mts.map} +1 -1
- package/dist/placeholder-LqmHqvBw.mjs +143 -0
- package/dist/placeholder-LqmHqvBw.mjs.map +1 -0
- package/dist/plugin-types.d.mts +122 -0
- package/dist/plugin-types.d.mts.map +1 -0
- package/dist/plugin-types.mjs +1 -0
- package/dist/plugins/adapt-sandbox-entry.d.mts +20 -12
- package/dist/plugins/adapt-sandbox-entry.d.mts.map +1 -1
- package/dist/plugins/adapt-sandbox-entry.mjs +46 -23
- package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -1
- package/dist/preview-C1LOEbWZ.mjs +107 -0
- package/dist/preview-C1LOEbWZ.mjs.map +1 -0
- package/dist/{public-url-B1AxbbbQ.mjs → public-url-CseXl9Fv.mjs} +39 -2
- package/dist/{public-url-B1AxbbbQ.mjs.map → public-url-CseXl9Fv.mjs.map} +1 -1
- package/dist/{query-yA3-rFji.mjs → query-axZmO6Tn.mjs} +12 -12
- package/dist/{query-yA3-rFji.mjs.map → query-axZmO6Tn.mjs.map} +1 -1
- package/dist/rate-limit-t5CVjCO6.mjs +120 -0
- package/dist/rate-limit-t5CVjCO6.mjs.map +1 -0
- package/dist/redirect-DGRsLO2I.mjs +17 -0
- package/dist/redirect-DGRsLO2I.mjs.map +1 -0
- package/dist/{redirect-C5H7VGIX.mjs → redirect-DkaDxq8e.mjs} +3 -3
- package/dist/{redirect-C5H7VGIX.mjs.map → redirect-DkaDxq8e.mjs.map} +1 -1
- package/dist/redirects-D1fdd68T.mjs +573 -0
- package/dist/redirects-D1fdd68T.mjs.map +1 -0
- package/dist/redirects-Dmj6KRU3.mjs +1141 -0
- package/dist/redirects-Dmj6KRU3.mjs.map +1 -0
- package/dist/{registry-Do34mz_P.mjs → registry-BnCeHYsf.mjs} +8 -300
- package/dist/registry-BnCeHYsf.mjs.map +1 -0
- package/dist/{request-cache-D4I69LeL.mjs → request-cache-dzCt8TZB.mjs} +1 -1
- package/dist/{request-cache-D4I69LeL.mjs.map → request-cache-dzCt8TZB.mjs.map} +1 -1
- package/dist/request-meta-CLCwSQOS.mjs +140 -0
- package/dist/request-meta-CLCwSQOS.mjs.map +1 -0
- package/dist/{runner-Iu3IZSDM.d.mts → runner-DcfZewkO.d.mts} +2 -2
- package/dist/{runner-Iu3IZSDM.d.mts.map → runner-DcfZewkO.d.mts.map} +1 -1
- package/dist/{runner-DIcU2UCC.mjs → runner-DdnQIwz_.mjs} +436 -187
- package/dist/runner-DdnQIwz_.mjs.map +1 -0
- package/dist/runtime.d.mts +10 -6
- package/dist/runtime.d.mts.map +1 -1
- package/dist/runtime.mjs +3 -3
- package/dist/schema-BmqagCwG.mjs +41 -0
- package/dist/schema-BmqagCwG.mjs.map +1 -0
- package/dist/search-CPrvO5u8.mjs +376 -0
- package/dist/search-CPrvO5u8.mjs.map +1 -0
- package/dist/{secrets-CZ8rxLX3.mjs → secrets-6pgZyq0K.mjs} +3 -3
- package/dist/{secrets-CZ8rxLX3.mjs.map → secrets-6pgZyq0K.mjs.map} +1 -1
- package/dist/sections-Cm-zb-gZ.mjs +346 -0
- package/dist/sections-Cm-zb-gZ.mjs.map +1 -0
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +19 -15
- package/dist/seo/index.d.mts +1 -1
- package/dist/seo-BoR4wCUh.mjs +86 -0
- package/dist/seo-BoR4wCUh.mjs.map +1 -0
- package/dist/seo-DRq9-EPP.mjs +130 -0
- package/dist/seo-DRq9-EPP.mjs.map +1 -0
- package/dist/service-vByySp-2.mjs +195 -0
- package/dist/service-vByySp-2.mjs.map +1 -0
- package/dist/settings-CBBj7HUd.mjs +51 -0
- package/dist/settings-CBBj7HUd.mjs.map +1 -0
- package/dist/settings-xQKsWnzQ.mjs +235 -0
- package/dist/settings-xQKsWnzQ.mjs.map +1 -0
- package/dist/setup-BGAJ2uXs.mjs +137 -0
- package/dist/setup-BGAJ2uXs.mjs.map +1 -0
- package/dist/setup-complete-C6ZCLhKo.mjs +26 -0
- package/dist/setup-complete-C6ZCLhKo.mjs.map +1 -0
- package/dist/setup-nonce-CY1gQiAU.mjs +25 -0
- package/dist/setup-nonce-CY1gQiAU.mjs.map +1 -0
- package/dist/site-url-D-M4Fd8O.mjs +13 -0
- package/dist/site-url-D-M4Fd8O.mjs.map +1 -0
- package/dist/slugify-Cjh1ssOZ.mjs +30 -0
- package/dist/slugify-Cjh1ssOZ.mjs.map +1 -0
- package/dist/ssrf-CTul4uQi.mjs +1 -0
- package/dist/ssrf-DzFN_qV-.mjs +332 -0
- package/dist/ssrf-DzFN_qV-.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/{taxonomies-JmQQZiG1.mjs → taxonomies-Cn9UpaR2.mjs} +7 -7
- package/dist/{taxonomies-JmQQZiG1.mjs.map → taxonomies-Cn9UpaR2.mjs.map} +1 -1
- package/dist/taxonomies-Dc0mzlms.mjs +508 -0
- package/dist/taxonomies-Dc0mzlms.mjs.map +1 -0
- package/dist/{taxonomy-D6NvlKo8.mjs → taxonomy-wPfusMK9.mjs} +3 -3
- package/dist/{taxonomy-D6NvlKo8.mjs.map → taxonomy-wPfusMK9.mjs.map} +1 -1
- package/dist/{tokens-CyRDPVW2.mjs → tokens-DILYNZMi.mjs} +2 -2
- package/dist/{tokens-CyRDPVW2.mjs.map → tokens-DILYNZMi.mjs.map} +1 -1
- package/dist/{transaction-D44LBXvU.mjs → transaction-NQj4VJ7Z.mjs} +1 -1
- package/dist/{transaction-D44LBXvU.mjs.map → transaction-NQj4VJ7Z.mjs.map} +1 -1
- package/dist/{transport-DX_5rpsq.d.mts → transport-GeXlLscf.d.mts} +1 -1
- package/dist/{transport-DX_5rpsq.d.mts.map → transport-GeXlLscf.d.mts.map} +1 -1
- package/dist/{transport-xpzIjCIB.mjs → transport-fw-mKJzT.mjs} +1 -1
- package/dist/{transport-xpzIjCIB.mjs.map → transport-fw-mKJzT.mjs.map} +1 -1
- package/dist/trusted-proxy-CJhQIk65.mjs +51 -0
- package/dist/trusted-proxy-CJhQIk65.mjs.map +1 -0
- package/dist/{types-DgSc9Rpc.d.mts → types-B05e2naf.d.mts} +5 -59
- package/dist/types-B05e2naf.d.mts.map +1 -0
- package/dist/{types-B1gLSAH2.d.mts → types-BWhaSS7U.d.mts} +2 -75
- package/dist/types-BWhaSS7U.d.mts.map +1 -0
- package/dist/{types-BQx6ZXpR.d.mts → types-C1KKK4VP.d.mts} +3 -1
- package/dist/{types-BQx6ZXpR.d.mts.map → types-C1KKK4VP.d.mts.map} +1 -1
- package/dist/types-Cb2UCDJg.d.mts +345 -0
- package/dist/types-Cb2UCDJg.d.mts.map +1 -0
- package/dist/{types-BIgulNsW.mjs → types-CwXMEPRr.mjs} +10 -3
- package/dist/types-CwXMEPRr.mjs.map +1 -0
- package/dist/{types-B_CXXnzh.d.mts → types-CzvJd1ND.d.mts} +7 -1
- package/dist/{types-B_CXXnzh.d.mts.map → types-CzvJd1ND.d.mts.map} +1 -1
- package/dist/types-DFowNO60.d.mts +198 -0
- package/dist/types-DFowNO60.d.mts.map +1 -0
- package/dist/{types-56BKbld_.mjs → types-DSZl1Dsv.mjs} +1 -1
- package/dist/{types-56BKbld_.mjs.map → types-DSZl1Dsv.mjs.map} +1 -1
- package/dist/types-DW1l0gCv.d.mts +75 -0
- package/dist/types-DW1l0gCv.d.mts.map +1 -0
- package/dist/types-Db67HHlU.mjs +3 -0
- package/dist/{types-C-aFbqmA.d.mts → types-DmxPPXGf.d.mts} +1 -1
- package/dist/{types-C-aFbqmA.d.mts.map → types-DmxPPXGf.d.mts.map} +1 -1
- package/dist/{types-PafqtQuM.mjs → types-Dz9CGX_d.mjs} +1 -1
- package/dist/{types-PafqtQuM.mjs.map → types-Dz9CGX_d.mjs.map} +1 -1
- package/dist/user-Dr1bOCqS.mjs +155 -0
- package/dist/user-Dr1bOCqS.mjs.map +1 -0
- package/dist/utils-_F-rWBTN.mjs +286 -0
- package/dist/utils-_F-rWBTN.mjs.map +1 -0
- package/dist/{validate-BcC3m2O7.d.mts → validate-BpQGsmd7.d.mts} +5 -4
- package/dist/validate-BpQGsmd7.d.mts.map +1 -0
- package/dist/{validate-UK4Ja1uo.mjs → validate-DlFxcVVK.mjs} +3 -3
- package/dist/{validate-UK4Ja1uo.mjs.map → validate-DlFxcVVK.mjs.map} +1 -1
- package/dist/{validation-Vc5DQkJa.mjs → validation-BiFJqUp5.mjs} +6 -5
- package/dist/{validation-Vc5DQkJa.mjs.map → validation-BiFJqUp5.mjs.map} +1 -1
- package/dist/version-Dw7Z5PVU.mjs +7 -0
- package/dist/{version-BdP--J1g.mjs.map → version-Dw7Z5PVU.mjs.map} +1 -1
- package/dist/widgets-B9j_yzlk.mjs +106 -0
- package/dist/widgets-B9j_yzlk.mjs.map +1 -0
- package/dist/zod-generator-DSyz01KE.mjs +234 -0
- package/dist/zod-generator-DSyz01KE.mjs.map +1 -0
- package/locals.d.ts +1 -1
- package/package.json +37 -14
- package/src/api/handlers/content.ts +1 -0
- package/src/api/handlers/index.ts +7 -0
- package/src/api/handlers/marketplace.ts +27 -6
- package/src/api/handlers/menus.ts +157 -580
- package/src/api/handlers/plugins.ts +77 -31
- package/src/api/handlers/registry.ts +1086 -0
- package/src/api/openapi/document.ts +10 -4
- package/src/api/schemas/content.ts +1 -0
- package/src/api/schemas/menus.ts +27 -23
- package/src/api/types.ts +6 -0
- package/src/astro/integration/index.ts +1 -0
- package/src/astro/integration/route-naming.ts +19 -0
- package/src/astro/integration/routes.ts +25 -3
- package/src/astro/integration/runtime.ts +35 -8
- package/src/astro/middleware/auth.ts +8 -2
- package/src/astro/middleware/csp.ts +25 -3
- package/src/astro/middleware.ts +3 -0
- package/src/astro/routes/api/admin/plugins/[id]/enable.ts +10 -0
- package/src/astro/routes/api/admin/plugins/registry/install.ts +107 -0
- package/src/astro/routes/api/auth/invite/register-options.ts +8 -1
- package/src/astro/routes/api/import/wordpress/execute.ts +185 -6
- package/src/astro/routes/api/menus/[name]/items/[id].ts +69 -0
- package/src/astro/routes/api/menus/[name]/items.ts +4 -65
- package/src/astro/types.ts +38 -0
- package/src/cli/wxr/parser.ts +263 -0
- package/src/client/index.ts +2 -1
- package/src/database/migrations/036_i18n_menus_and_taxonomies.ts +166 -49
- package/src/database/migrations/038_registry_plugin_state.ts +130 -0
- package/src/database/migrations/039_fix_fts5_triggers.ts +264 -0
- package/src/database/migrations/runner.ts +4 -0
- package/src/database/repositories/content.ts +5 -1
- package/src/database/repositories/index.ts +14 -0
- package/src/database/repositories/menu.ts +644 -0
- package/src/database/repositories/types.ts +6 -0
- package/src/database/types.ts +5 -1
- package/src/emdash-runtime.ts +122 -34
- package/src/import/sources/wordpress-plugin.ts +9 -2
- package/src/import/sources/wxr.ts +16 -2
- package/src/import/ssrf.ts +20 -500
- package/src/import/wxr-taxonomies.ts +730 -0
- package/src/index.ts +3 -10
- package/src/media/normalize.ts +37 -4
- package/src/plugin-types.ts +240 -0
- package/src/plugins/adapt-sandbox-entry.ts +115 -39
- package/src/plugins/define-plugin.ts +34 -56
- package/src/plugins/index.ts +1 -9
- package/src/plugins/marketplace.ts +63 -4
- package/src/plugins/sandbox/index.ts +1 -1
- package/src/plugins/sandbox/noop.ts +2 -2
- package/src/plugins/sandbox/types.ts +7 -4
- package/src/plugins/state.ts +84 -38
- package/src/plugins/types.ts +2 -79
- package/src/registry/config.ts +311 -0
- package/src/registry/plugin-id.ts +116 -0
- package/src/registry/types.ts +206 -0
- package/src/search/fts-manager.ts +77 -15
- package/src/security/ssrf.ts +501 -0
- package/dist/apply-C1ZORgcy.mjs.map +0 -1
- package/dist/content-CERxPUN0.mjs.map +0 -1
- package/dist/error-D6LuHLw9.mjs +0 -27
- package/dist/error-D6LuHLw9.mjs.map +0 -1
- package/dist/index-Dlkzhb4C.d.mts.map +0 -1
- package/dist/placeholder-Ci0RLeCk.mjs +0 -268
- package/dist/placeholder-Ci0RLeCk.mjs.map +0 -1
- package/dist/registry-Do34mz_P.mjs.map +0 -1
- package/dist/runner-DIcU2UCC.mjs.map +0 -1
- package/dist/search-n-ZCMfr3.mjs +0 -9914
- package/dist/search-n-ZCMfr3.mjs.map +0 -1
- package/dist/settings-nTXPRi3D.mjs +0 -440
- package/dist/settings-nTXPRi3D.mjs.map +0 -1
- package/dist/types-B1gLSAH2.d.mts.map +0 -1
- package/dist/types-BIgulNsW.mjs.map +0 -1
- package/dist/types-Cug_RO3W.mjs +0 -16
- package/dist/types-Cug_RO3W.mjs.map +0 -1
- package/dist/types-DgSc9Rpc.d.mts.map +0 -1
- package/dist/validate-BcC3m2O7.d.mts.map +0 -1
- package/dist/version-BdP--J1g.mjs +0 -7
- package/dist/zod-generator-CHnJUP2l.mjs +0 -137
- package/dist/zod-generator-CHnJUP2l.mjs.map +0 -1
|
@@ -0,0 +1,3941 @@
|
|
|
1
|
+
import { r as validatePluginIdentifier, t as validateIdentifier } from "./validate-VPnKoIzW.mjs";
|
|
2
|
+
import { r as isI18nEnabled } from "./config-CVssduLe.mjs";
|
|
3
|
+
import { r as RevisionRepository, t as ContentRepository } from "./content-D6YG26WG.mjs";
|
|
4
|
+
import { r as encodeBase64, t as decodeBase64 } from "./base64-CqR-7kqF.mjs";
|
|
5
|
+
import { n as InvalidCursorError, t as EmDashValidationError } from "./types-CwXMEPRr.mjs";
|
|
6
|
+
import { t as MediaRepository } from "./media-Dg7he9uK.mjs";
|
|
7
|
+
import { t as CommentRepository } from "./comment-Dd9MI82-.mjs";
|
|
8
|
+
import { t as withTransaction } from "./transaction-NQj4VJ7Z.mjs";
|
|
9
|
+
import { t as RedirectRepository } from "./redirect-DkaDxq8e.mjs";
|
|
10
|
+
import { n as chunks, t as SQL_BATCH_SIZE } from "./chunks-DyGtu1Bv.mjs";
|
|
11
|
+
import { t as BylineRepository } from "./byline-D09BaS4j.mjs";
|
|
12
|
+
import { t as SeoRepository } from "./seo-DRq9-EPP.mjs";
|
|
13
|
+
import { r as invalidateRedirectCache } from "./cache-CXCpjWiL.mjs";
|
|
14
|
+
import { t as isMissingTableError } from "./db-errors-CGN9kJfo.mjs";
|
|
15
|
+
import { r as parseAllowedMimeTypes, t as matchesMimeAllowlist } from "./mime-KV5TqkMN.mjs";
|
|
16
|
+
import { n as requestCached } from "./request-cache-dzCt8TZB.mjs";
|
|
17
|
+
import { n as hashString } from "./hash-DlUxGhQS.mjs";
|
|
18
|
+
import { n as SchemaRegistry, t as SchemaError } from "./registry-BnCeHYsf.mjs";
|
|
19
|
+
import { i as pluginManifestSchema, r as normalizeManifestRoute } from "./manifest-schema-HCtSh4Jq.mjs";
|
|
20
|
+
import { r as normalizeCapabilities } from "./types-Db67HHlU.mjs";
|
|
21
|
+
import { t as EmDashStorageError } from "./types-Dz9CGX_d.mjs";
|
|
22
|
+
import { n as resolveAndValidateExternalUrl, t as SsrfError } from "./ssrf-DzFN_qV-.mjs";
|
|
23
|
+
import { sql } from "kysely";
|
|
24
|
+
import { createGzipDecoder, unpackTar } from "modern-tar";
|
|
25
|
+
|
|
26
|
+
//#region src/api/rev.ts
|
|
27
|
+
/**
|
|
28
|
+
* Generate a _rev token from a content item's version and updatedAt.
|
|
29
|
+
*/
|
|
30
|
+
function encodeRev(item) {
|
|
31
|
+
return encodeBase64(`${item.version}:${item.updatedAt}`);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Decode a _rev token into its components.
|
|
35
|
+
* Returns null if the token is malformed.
|
|
36
|
+
*/
|
|
37
|
+
function decodeRev(rev) {
|
|
38
|
+
try {
|
|
39
|
+
const decoded = decodeBase64(rev);
|
|
40
|
+
const colonIdx = decoded.indexOf(":");
|
|
41
|
+
if (colonIdx === -1) return null;
|
|
42
|
+
const version = parseInt(decoded.slice(0, colonIdx), 10);
|
|
43
|
+
const updatedAt = decoded.slice(colonIdx + 1);
|
|
44
|
+
if (isNaN(version) || !updatedAt) return null;
|
|
45
|
+
return {
|
|
46
|
+
version,
|
|
47
|
+
updatedAt
|
|
48
|
+
};
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Validate a _rev token against a content item.
|
|
55
|
+
* Returns null if valid (or if no _rev provided), or an error message if invalid.
|
|
56
|
+
*/
|
|
57
|
+
function validateRev(rev, item) {
|
|
58
|
+
if (!rev) return { valid: true };
|
|
59
|
+
const decoded = decodeRev(rev);
|
|
60
|
+
if (!decoded) return {
|
|
61
|
+
valid: false,
|
|
62
|
+
message: "Malformed _rev token"
|
|
63
|
+
};
|
|
64
|
+
if (decoded.version !== item.version || decoded.updatedAt !== item.updatedAt) return {
|
|
65
|
+
valid: false,
|
|
66
|
+
message: "Content has been modified since last read (version conflict)"
|
|
67
|
+
};
|
|
68
|
+
return { valid: true };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/api/handlers/validate-media-fields.ts
|
|
73
|
+
function asMediaRef(value) {
|
|
74
|
+
if (value === null || value === void 0) return null;
|
|
75
|
+
if (typeof value !== "object" || Array.isArray(value)) return null;
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
function fail(message) {
|
|
79
|
+
return {
|
|
80
|
+
success: false,
|
|
81
|
+
error: {
|
|
82
|
+
code: "INVALID_MIME_FOR_FIELD",
|
|
83
|
+
message
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
async function loadMediaFieldsForCollection(db, collectionSlug) {
|
|
88
|
+
const rows = await db.selectFrom("_emdash_fields").innerJoin("_emdash_collections", "_emdash_collections.id", "_emdash_fields.collection_id").select([
|
|
89
|
+
"_emdash_fields.slug",
|
|
90
|
+
"_emdash_fields.type",
|
|
91
|
+
"_emdash_fields.validation"
|
|
92
|
+
]).where("_emdash_collections.slug", "=", collectionSlug).where("_emdash_fields.type", "in", ["file", "image"]).execute();
|
|
93
|
+
const out = [];
|
|
94
|
+
for (const row of rows) {
|
|
95
|
+
const list = parseAllowedMimeTypes(row.validation);
|
|
96
|
+
if (!list) continue;
|
|
97
|
+
out.push({
|
|
98
|
+
slug: row.slug,
|
|
99
|
+
type: row.type,
|
|
100
|
+
allowedMimeTypes: list
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
async function validateMediaFields(db, collectionSlug, data) {
|
|
106
|
+
const fields = await requestCached(`mediaFields:${collectionSlug}`, () => loadMediaFieldsForCollection(db, collectionSlug));
|
|
107
|
+
if (fields.length === 0) return {
|
|
108
|
+
success: true,
|
|
109
|
+
data: true
|
|
110
|
+
};
|
|
111
|
+
const localIds = /* @__PURE__ */ new Set();
|
|
112
|
+
for (const field of fields) {
|
|
113
|
+
const ref = asMediaRef(data[field.slug]);
|
|
114
|
+
if (!ref) continue;
|
|
115
|
+
if ((typeof ref.provider === "string" ? ref.provider : "local") === "local" && typeof ref.id === "string") localIds.add(ref.id);
|
|
116
|
+
}
|
|
117
|
+
const idList = [...localIds];
|
|
118
|
+
const mimeById = /* @__PURE__ */ new Map();
|
|
119
|
+
if (idList.length > 0) for (const batch of chunks(idList, SQL_BATCH_SIZE)) {
|
|
120
|
+
const rows = await db.selectFrom("media").select(["id", "mime_type"]).where("id", "in", batch).execute();
|
|
121
|
+
for (const r of rows) mimeById.set(r.id, r.mime_type);
|
|
122
|
+
}
|
|
123
|
+
for (const field of fields) {
|
|
124
|
+
const value = data[field.slug];
|
|
125
|
+
if (value === null || value === void 0) continue;
|
|
126
|
+
const ref = asMediaRef(value);
|
|
127
|
+
if (!ref) continue;
|
|
128
|
+
const provider = typeof ref.provider === "string" ? ref.provider : "local";
|
|
129
|
+
let mime;
|
|
130
|
+
if (provider === "local") {
|
|
131
|
+
if (typeof ref.id !== "string") return fail(`Field '${field.slug}' references media with an invalid id`);
|
|
132
|
+
mime = mimeById.get(ref.id);
|
|
133
|
+
if (!mime) return fail(`Field '${field.slug}' references media with unknown MIME type`);
|
|
134
|
+
} else {
|
|
135
|
+
if (typeof ref.mimeType !== "string") return fail(`Field '${field.slug}' requires a mimeType declaration for non-local media`);
|
|
136
|
+
mime = ref.mimeType;
|
|
137
|
+
}
|
|
138
|
+
if (!matchesMimeAllowlist(mime, field.allowedMimeTypes)) return fail(`Field '${field.slug}' does not accept ${mime}`);
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
success: true,
|
|
142
|
+
data: true
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
//#endregion
|
|
147
|
+
//#region src/api/handlers/content.ts
|
|
148
|
+
/**
|
|
149
|
+
* Narrow a caught error to one carrying a structured `apiError` discriminant.
|
|
150
|
+
* Used by transaction callbacks that want to surface a specific error code
|
|
151
|
+
* through the standard Error throwing path.
|
|
152
|
+
*/
|
|
153
|
+
function hasApiError(error) {
|
|
154
|
+
if (!(error instanceof Error) || !("apiError" in error)) return false;
|
|
155
|
+
const { apiError } = error;
|
|
156
|
+
return typeof apiError === "object" && apiError !== null && "code" in apiError && typeof apiError.code === "string";
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Extract a slug source (title or name) from content data.
|
|
160
|
+
* Returns null if no suitable string field is found.
|
|
161
|
+
*/
|
|
162
|
+
function getSlugSource(data) {
|
|
163
|
+
if (typeof data.title === "string" && data.title.length > 0) return data.title;
|
|
164
|
+
if (typeof data.name === "string" && data.name.length > 0) return data.name;
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
/** Default SEO values for content without an explicit SEO row */
|
|
168
|
+
const SEO_DEFAULTS = {
|
|
169
|
+
title: null,
|
|
170
|
+
description: null,
|
|
171
|
+
image: null,
|
|
172
|
+
canonical: null,
|
|
173
|
+
noIndex: false
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* Check if a collection has SEO enabled.
|
|
177
|
+
*/
|
|
178
|
+
async function collectionHasSeo(db, collection) {
|
|
179
|
+
return (await db.selectFrom("_emdash_collections").select("has_seo").where("slug", "=", collection).executeTakeFirst())?.has_seo === 1;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Hydrate SEO data on a single content item if the collection has SEO enabled.
|
|
183
|
+
*/
|
|
184
|
+
async function hydrateSeo(db, collection, item, hasSeo) {
|
|
185
|
+
if (!hasSeo) return;
|
|
186
|
+
item.seo = await new SeoRepository(db).get(collection, item.id);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Hydrate SEO data on multiple content items using a single batch query.
|
|
190
|
+
*/
|
|
191
|
+
async function hydrateSeoMany(db, collection, items, hasSeo) {
|
|
192
|
+
if (!hasSeo || items.length === 0) return;
|
|
193
|
+
const seoMap = await new SeoRepository(db).getMany(collection, items.map((i) => i.id));
|
|
194
|
+
for (const item of items) item.seo = seoMap.get(item.id) ?? { ...SEO_DEFAULTS };
|
|
195
|
+
}
|
|
196
|
+
async function hydrateBylines(db, collection, item) {
|
|
197
|
+
const bylineRepo = new BylineRepository(db);
|
|
198
|
+
const bylines = await bylineRepo.getContentBylines(collection, item.id);
|
|
199
|
+
if (bylines.length > 0) {
|
|
200
|
+
item.bylines = bylines.map((c) => ({
|
|
201
|
+
...c,
|
|
202
|
+
source: "explicit"
|
|
203
|
+
}));
|
|
204
|
+
item.byline = bylines[0]?.byline ?? null;
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (item.primaryBylineId) item.primaryBylineId = null;
|
|
208
|
+
if (item.authorId) {
|
|
209
|
+
const fallback = await bylineRepo.findByUserId(item.authorId);
|
|
210
|
+
if (fallback) {
|
|
211
|
+
item.bylines = [{
|
|
212
|
+
byline: fallback,
|
|
213
|
+
sortOrder: 0,
|
|
214
|
+
roleLabel: null,
|
|
215
|
+
source: "inferred"
|
|
216
|
+
}];
|
|
217
|
+
item.byline = fallback;
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
item.bylines = [];
|
|
222
|
+
item.byline = null;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Batch-hydrate bylines for multiple items using two bulk queries instead of N+1.
|
|
226
|
+
*/
|
|
227
|
+
async function hydrateBylinesMany(db, collection, items) {
|
|
228
|
+
if (items.length === 0) return;
|
|
229
|
+
const bylineRepo = new BylineRepository(db);
|
|
230
|
+
const contentIds = items.map((i) => i.id);
|
|
231
|
+
const bylinesMap = await bylineRepo.getContentBylinesMany(collection, contentIds);
|
|
232
|
+
const fallbackAuthorIds = [];
|
|
233
|
+
for (const item of items) if (!bylinesMap.has(item.id) && item.authorId) fallbackAuthorIds.push(item.authorId);
|
|
234
|
+
const uniqueAuthorIds = [...new Set(fallbackAuthorIds)];
|
|
235
|
+
const authorBylineMap = await bylineRepo.findByUserIds(uniqueAuthorIds);
|
|
236
|
+
for (const item of items) {
|
|
237
|
+
const explicit = bylinesMap.get(item.id);
|
|
238
|
+
if (explicit && explicit.length > 0) {
|
|
239
|
+
item.bylines = explicit.map((c) => ({
|
|
240
|
+
...c,
|
|
241
|
+
source: "explicit"
|
|
242
|
+
}));
|
|
243
|
+
item.byline = explicit[0]?.byline ?? null;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (item.primaryBylineId) item.primaryBylineId = null;
|
|
247
|
+
if (item.authorId) {
|
|
248
|
+
const fallback = authorBylineMap.get(item.authorId);
|
|
249
|
+
if (fallback) {
|
|
250
|
+
item.bylines = [{
|
|
251
|
+
byline: fallback,
|
|
252
|
+
sortOrder: 0,
|
|
253
|
+
roleLabel: null,
|
|
254
|
+
source: "inferred"
|
|
255
|
+
}];
|
|
256
|
+
item.byline = fallback;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
item.bylines = [];
|
|
261
|
+
item.byline = null;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Resolve an identifier (ID or slug) to a real content ID.
|
|
266
|
+
* Returns the ID if found, null if not found.
|
|
267
|
+
* When locale is provided, slug lookups are scoped to that locale.
|
|
268
|
+
*/
|
|
269
|
+
async function resolveId(repo, collection, identifier, locale) {
|
|
270
|
+
return (await repo.findByIdOrSlug(collection, identifier, locale))?.id ?? null;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Resolve an identifier (ID or slug) to a real content ID,
|
|
274
|
+
* including trashed (soft-deleted) items.
|
|
275
|
+
*/
|
|
276
|
+
async function resolveIdIncludingTrashed(repo, collection, identifier, locale) {
|
|
277
|
+
return (await repo.findByIdOrSlugIncludingTrashed(collection, identifier, locale))?.id ?? null;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Create content list handler
|
|
281
|
+
*/
|
|
282
|
+
async function handleContentList(db, collection, params) {
|
|
283
|
+
try {
|
|
284
|
+
const repo = new ContentRepository(db);
|
|
285
|
+
const where = {};
|
|
286
|
+
if (params.status) where.status = params.status;
|
|
287
|
+
if (params.locale) where.locale = params.locale;
|
|
288
|
+
const result = await repo.findMany(collection, {
|
|
289
|
+
cursor: params.cursor,
|
|
290
|
+
limit: params.limit || 50,
|
|
291
|
+
where: Object.keys(where).length > 0 ? where : void 0,
|
|
292
|
+
orderBy: params.orderBy ? {
|
|
293
|
+
field: params.orderBy,
|
|
294
|
+
direction: params.order || "desc"
|
|
295
|
+
} : void 0
|
|
296
|
+
});
|
|
297
|
+
const hasSeo = await collectionHasSeo(db, collection);
|
|
298
|
+
await hydrateSeoMany(db, collection, result.items, hasSeo);
|
|
299
|
+
await hydrateBylinesMany(db, collection, result.items);
|
|
300
|
+
return {
|
|
301
|
+
success: true,
|
|
302
|
+
data: {
|
|
303
|
+
items: result.items,
|
|
304
|
+
nextCursor: result.nextCursor,
|
|
305
|
+
total: result.total
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
} catch (error) {
|
|
309
|
+
if (error instanceof InvalidCursorError) return {
|
|
310
|
+
success: false,
|
|
311
|
+
error: {
|
|
312
|
+
code: "INVALID_CURSOR",
|
|
313
|
+
message: error.message
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
if (isMissingTableError(error)) return {
|
|
317
|
+
success: false,
|
|
318
|
+
error: {
|
|
319
|
+
code: "COLLECTION_NOT_FOUND",
|
|
320
|
+
message: `Collection '${collection}' not found`
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
if (error instanceof EmDashValidationError) return {
|
|
324
|
+
success: false,
|
|
325
|
+
error: {
|
|
326
|
+
code: "VALIDATION_ERROR",
|
|
327
|
+
message: error.message
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
console.error("Content list error:", error);
|
|
331
|
+
return {
|
|
332
|
+
success: false,
|
|
333
|
+
error: {
|
|
334
|
+
code: "CONTENT_LIST_ERROR",
|
|
335
|
+
message: "Failed to list content"
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Get single content item
|
|
342
|
+
*/
|
|
343
|
+
async function handleContentGet(db, collection, id, locale) {
|
|
344
|
+
try {
|
|
345
|
+
const item = await new ContentRepository(db).findByIdOrSlug(collection, id, locale);
|
|
346
|
+
if (!item) return {
|
|
347
|
+
success: false,
|
|
348
|
+
error: {
|
|
349
|
+
code: "NOT_FOUND",
|
|
350
|
+
message: `Content item not found: ${id}`
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
await hydrateSeo(db, collection, item, await collectionHasSeo(db, collection));
|
|
354
|
+
await hydrateBylines(db, collection, item);
|
|
355
|
+
return {
|
|
356
|
+
success: true,
|
|
357
|
+
data: {
|
|
358
|
+
item,
|
|
359
|
+
_rev: encodeRev(item)
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
} catch (error) {
|
|
363
|
+
console.error("Content get error:", error);
|
|
364
|
+
return {
|
|
365
|
+
success: false,
|
|
366
|
+
error: {
|
|
367
|
+
code: "CONTENT_GET_ERROR",
|
|
368
|
+
message: "Failed to get content"
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Get a content item by id, including trashed items.
|
|
375
|
+
* Used by restore endpoint for ownership checks on soft-deleted items.
|
|
376
|
+
*/
|
|
377
|
+
async function handleContentGetIncludingTrashed(db, collection, id, locale) {
|
|
378
|
+
try {
|
|
379
|
+
const item = await new ContentRepository(db).findByIdOrSlugIncludingTrashed(collection, id, locale);
|
|
380
|
+
if (!item) return {
|
|
381
|
+
success: false,
|
|
382
|
+
error: {
|
|
383
|
+
code: "NOT_FOUND",
|
|
384
|
+
message: `Content item not found: ${id}`
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
await hydrateSeo(db, collection, item, await collectionHasSeo(db, collection));
|
|
388
|
+
await hydrateBylines(db, collection, item);
|
|
389
|
+
return {
|
|
390
|
+
success: true,
|
|
391
|
+
data: {
|
|
392
|
+
item,
|
|
393
|
+
_rev: encodeRev(item)
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
} catch (error) {
|
|
397
|
+
console.error("Content get error:", error);
|
|
398
|
+
return {
|
|
399
|
+
success: false,
|
|
400
|
+
error: {
|
|
401
|
+
code: "CONTENT_GET_ERROR",
|
|
402
|
+
message: "Failed to get content"
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Create content item.
|
|
409
|
+
*
|
|
410
|
+
* Content + SEO writes are wrapped in a transaction so either both succeed
|
|
411
|
+
* or neither does. If `body.seo` is provided for a non-SEO collection, the
|
|
412
|
+
* API returns a validation error rather than silently dropping it.
|
|
413
|
+
*/
|
|
414
|
+
async function handleContentCreate(db, collection, body) {
|
|
415
|
+
try {
|
|
416
|
+
const hasSeo = await collectionHasSeo(db, collection);
|
|
417
|
+
if (body.seo && !hasSeo) return {
|
|
418
|
+
success: false,
|
|
419
|
+
error: {
|
|
420
|
+
code: "VALIDATION_ERROR",
|
|
421
|
+
message: `Collection "${collection}" does not have SEO enabled. Remove the seo field or enable SEO on this collection.`
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
const mimeCheck = await validateMediaFields(db, collection, body.data);
|
|
425
|
+
if (!mimeCheck.success) return mimeCheck;
|
|
426
|
+
const item = await withTransaction(db, async (trx) => {
|
|
427
|
+
const repo = new ContentRepository(trx);
|
|
428
|
+
const bylineRepo = new BylineRepository(trx);
|
|
429
|
+
let slug = body.slug;
|
|
430
|
+
if (!slug) {
|
|
431
|
+
const slugSource = getSlugSource(body.data);
|
|
432
|
+
if (slugSource) slug = await repo.generateUniqueSlug(collection, slugSource, body.locale);
|
|
433
|
+
}
|
|
434
|
+
const created = await repo.create({
|
|
435
|
+
type: collection,
|
|
436
|
+
slug,
|
|
437
|
+
data: body.data,
|
|
438
|
+
status: body.status || "draft",
|
|
439
|
+
authorId: body.authorId,
|
|
440
|
+
locale: body.locale,
|
|
441
|
+
translationOf: body.translationOf,
|
|
442
|
+
createdAt: body.createdAt,
|
|
443
|
+
publishedAt: body.publishedAt
|
|
444
|
+
});
|
|
445
|
+
if (body.bylines !== void 0) {
|
|
446
|
+
await bylineRepo.setContentBylines(collection, created.id, body.bylines);
|
|
447
|
+
created.primaryBylineId = body.bylines[0]?.bylineId ?? null;
|
|
448
|
+
}
|
|
449
|
+
await hydrateBylines(trx, collection, created);
|
|
450
|
+
if (body.translationOf) {
|
|
451
|
+
const { TaxonomyRepository } = await import("./taxonomy-wPfusMK9.mjs").then((n) => n.n);
|
|
452
|
+
await new TaxonomyRepository(trx).copyEntryTerms(collection, body.translationOf, created.id);
|
|
453
|
+
}
|
|
454
|
+
if (body.seo && hasSeo) created.seo = await new SeoRepository(trx).upsert(collection, created.id, body.seo);
|
|
455
|
+
else if (hasSeo) created.seo = { ...SEO_DEFAULTS };
|
|
456
|
+
return created;
|
|
457
|
+
});
|
|
458
|
+
return {
|
|
459
|
+
success: true,
|
|
460
|
+
data: {
|
|
461
|
+
item,
|
|
462
|
+
_rev: encodeRev(item)
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
} catch (error) {
|
|
466
|
+
if (isMissingTableError(error)) return {
|
|
467
|
+
success: false,
|
|
468
|
+
error: {
|
|
469
|
+
code: "COLLECTION_NOT_FOUND",
|
|
470
|
+
message: `Collection '${collection}' not found`
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
if (error instanceof EmDashValidationError) return {
|
|
474
|
+
success: false,
|
|
475
|
+
error: {
|
|
476
|
+
code: "VALIDATION_ERROR",
|
|
477
|
+
message: error.message
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
const message = error instanceof Error ? error.message.toLowerCase() : "";
|
|
481
|
+
if (message.includes("unique constraint failed") || message.includes("duplicate key")) {
|
|
482
|
+
if (message.includes("slug")) return {
|
|
483
|
+
success: false,
|
|
484
|
+
error: {
|
|
485
|
+
code: "SLUG_CONFLICT",
|
|
486
|
+
message: `Slug '${body.slug ?? "(auto-generated)"}' already exists in collection '${collection}'`
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
return {
|
|
490
|
+
success: false,
|
|
491
|
+
error: {
|
|
492
|
+
code: "CONFLICT",
|
|
493
|
+
message: "Unique constraint violation"
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
console.error("Content create error:", error);
|
|
498
|
+
return {
|
|
499
|
+
success: false,
|
|
500
|
+
error: {
|
|
501
|
+
code: "CONTENT_CREATE_ERROR",
|
|
502
|
+
message: "Failed to create content"
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Update content item.
|
|
509
|
+
* If `_rev` is provided, validates it against the current version before writing.
|
|
510
|
+
* No `_rev` = blind write (backwards-compatible for admin UI).
|
|
511
|
+
*
|
|
512
|
+
* Content + SEO writes are wrapped in a transaction for atomicity.
|
|
513
|
+
*/
|
|
514
|
+
async function handleContentUpdate(db, collection, id, body) {
|
|
515
|
+
try {
|
|
516
|
+
const hasSeo = await collectionHasSeo(db, collection);
|
|
517
|
+
if (body.seo && !hasSeo) return {
|
|
518
|
+
success: false,
|
|
519
|
+
error: {
|
|
520
|
+
code: "VALIDATION_ERROR",
|
|
521
|
+
message: `Collection "${collection}" does not have SEO enabled. Remove the seo field or enable SEO on this collection.`
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
if (body.data) {
|
|
525
|
+
const mimeCheck = await validateMediaFields(db, collection, body.data);
|
|
526
|
+
if (!mimeCheck.success) return mimeCheck;
|
|
527
|
+
}
|
|
528
|
+
const resolvedId = await resolveId(new ContentRepository(db), collection, id) ?? id;
|
|
529
|
+
const item = await withTransaction(db, async (trx) => {
|
|
530
|
+
const trxRepo = new ContentRepository(trx);
|
|
531
|
+
const bylineRepo = new BylineRepository(trx);
|
|
532
|
+
const existing = body._rev || body.slug ? await trxRepo.findById(collection, resolvedId) : null;
|
|
533
|
+
if (body._rev) {
|
|
534
|
+
if (!existing) throw Object.assign(/* @__PURE__ */ new Error(`Content item not found: ${id}`), { apiError: { code: "NOT_FOUND" } });
|
|
535
|
+
const revCheck = validateRev(body._rev, existing);
|
|
536
|
+
if (!revCheck.valid) throw Object.assign(new Error(revCheck.message), { apiError: { code: "CONFLICT" } });
|
|
537
|
+
}
|
|
538
|
+
let oldSlug;
|
|
539
|
+
if (body.slug && existing?.slug && existing.slug !== body.slug) oldSlug = existing.slug;
|
|
540
|
+
const updated = await trxRepo.update(collection, resolvedId, {
|
|
541
|
+
data: body.data,
|
|
542
|
+
slug: body.slug,
|
|
543
|
+
status: body.status,
|
|
544
|
+
authorId: body.authorId,
|
|
545
|
+
publishedAt: body.publishedAt
|
|
546
|
+
});
|
|
547
|
+
if (body.bylines !== void 0) {
|
|
548
|
+
await bylineRepo.setContentBylines(collection, resolvedId, body.bylines);
|
|
549
|
+
updated.primaryBylineId = body.bylines[0]?.bylineId ?? null;
|
|
550
|
+
}
|
|
551
|
+
if (oldSlug && body.slug) {
|
|
552
|
+
const collectionRow = await trx.selectFrom("_emdash_collections").select("url_pattern").where("slug", "=", collection).executeTakeFirst();
|
|
553
|
+
await new RedirectRepository(trx).createAutoRedirect(collection, oldSlug, body.slug, resolvedId, collectionRow?.url_pattern ?? null);
|
|
554
|
+
invalidateRedirectCache();
|
|
555
|
+
}
|
|
556
|
+
if (isI18nEnabled() && body.data && updated.translationGroup) await syncNonTranslatableFields(trx, collection, updated.id, updated.translationGroup, body.data);
|
|
557
|
+
if (body.seo && hasSeo) updated.seo = await new SeoRepository(trx).upsert(collection, resolvedId, body.seo);
|
|
558
|
+
else if (hasSeo) updated.seo = await new SeoRepository(trx).get(collection, resolvedId);
|
|
559
|
+
await hydrateBylines(trx, collection, updated);
|
|
560
|
+
return updated;
|
|
561
|
+
});
|
|
562
|
+
return {
|
|
563
|
+
success: true,
|
|
564
|
+
data: {
|
|
565
|
+
item,
|
|
566
|
+
_rev: encodeRev(item)
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
} catch (error) {
|
|
570
|
+
if (hasApiError(error)) return {
|
|
571
|
+
success: false,
|
|
572
|
+
error: {
|
|
573
|
+
code: error.apiError.code,
|
|
574
|
+
message: error.message
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
if (isMissingTableError(error)) return {
|
|
578
|
+
success: false,
|
|
579
|
+
error: {
|
|
580
|
+
code: "COLLECTION_NOT_FOUND",
|
|
581
|
+
message: `Collection '${collection}' not found`
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
if (error instanceof EmDashValidationError) return {
|
|
585
|
+
success: false,
|
|
586
|
+
error: {
|
|
587
|
+
code: "VALIDATION_ERROR",
|
|
588
|
+
message: error.message
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
const message = error instanceof Error ? error.message.toLowerCase() : "";
|
|
592
|
+
if (message.includes("unique constraint failed") || message.includes("duplicate key")) {
|
|
593
|
+
if (message.includes("slug")) return {
|
|
594
|
+
success: false,
|
|
595
|
+
error: {
|
|
596
|
+
code: "SLUG_CONFLICT",
|
|
597
|
+
message: `Slug '${body.slug ?? id}' already exists in collection '${collection}'`
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
return {
|
|
601
|
+
success: false,
|
|
602
|
+
error: {
|
|
603
|
+
code: "CONFLICT",
|
|
604
|
+
message: "Unique constraint violation"
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
console.error("Content update error:", error);
|
|
609
|
+
return {
|
|
610
|
+
success: false,
|
|
611
|
+
error: {
|
|
612
|
+
code: "CONTENT_UPDATE_ERROR",
|
|
613
|
+
message: "Failed to update content"
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Duplicate content item.
|
|
620
|
+
*
|
|
621
|
+
* Only copies SEO data if the collection has SEO enabled.
|
|
622
|
+
* Always returns consistent `seo` shape for SEO-enabled collections.
|
|
623
|
+
*/
|
|
624
|
+
async function handleContentDuplicate(db, collection, id, authorId) {
|
|
625
|
+
try {
|
|
626
|
+
const hasSeo = await collectionHasSeo(db, collection);
|
|
627
|
+
return {
|
|
628
|
+
success: true,
|
|
629
|
+
data: { item: await withTransaction(db, async (trx) => {
|
|
630
|
+
const repo = new ContentRepository(trx);
|
|
631
|
+
const bylineRepo = new BylineRepository(trx);
|
|
632
|
+
const resolvedId = await resolveId(repo, collection, id) ?? id;
|
|
633
|
+
const dup = await repo.duplicate(collection, resolvedId, authorId);
|
|
634
|
+
const existingBylines = await bylineRepo.getContentBylines(collection, resolvedId);
|
|
635
|
+
if (existingBylines.length > 0) await bylineRepo.setContentBylines(collection, dup.id, existingBylines.map((entry) => ({
|
|
636
|
+
bylineId: entry.byline.id,
|
|
637
|
+
roleLabel: entry.roleLabel
|
|
638
|
+
})));
|
|
639
|
+
if (hasSeo) {
|
|
640
|
+
const seoRepo = new SeoRepository(trx);
|
|
641
|
+
await seoRepo.copyForDuplicate(collection, resolvedId, dup.id);
|
|
642
|
+
dup.seo = await seoRepo.get(collection, dup.id);
|
|
643
|
+
}
|
|
644
|
+
await hydrateBylines(trx, collection, dup);
|
|
645
|
+
return dup;
|
|
646
|
+
}) }
|
|
647
|
+
};
|
|
648
|
+
} catch (err) {
|
|
649
|
+
if (err instanceof EmDashValidationError) return {
|
|
650
|
+
success: false,
|
|
651
|
+
error: {
|
|
652
|
+
code: "NOT_FOUND",
|
|
653
|
+
message: err.message
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
console.error("Content duplicate error:", err);
|
|
657
|
+
return {
|
|
658
|
+
success: false,
|
|
659
|
+
error: {
|
|
660
|
+
code: "CONTENT_DUPLICATE_ERROR",
|
|
661
|
+
message: "Failed to duplicate content"
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Delete content item (soft delete - moves to trash)
|
|
668
|
+
*/
|
|
669
|
+
async function handleContentDelete(db, collection, id) {
|
|
670
|
+
try {
|
|
671
|
+
if (!await withTransaction(db, async (trx) => {
|
|
672
|
+
const repo = new ContentRepository(trx);
|
|
673
|
+
const resolvedId = await resolveId(repo, collection, id) ?? id;
|
|
674
|
+
return repo.delete(collection, resolvedId);
|
|
675
|
+
})) return {
|
|
676
|
+
success: false,
|
|
677
|
+
error: {
|
|
678
|
+
code: "NOT_FOUND",
|
|
679
|
+
message: `Content item not found: ${id}`
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
return {
|
|
683
|
+
success: true,
|
|
684
|
+
data: { deleted: true }
|
|
685
|
+
};
|
|
686
|
+
} catch (error) {
|
|
687
|
+
console.error("Content delete error:", error);
|
|
688
|
+
return {
|
|
689
|
+
success: false,
|
|
690
|
+
error: {
|
|
691
|
+
code: "CONTENT_DELETE_ERROR",
|
|
692
|
+
message: "Failed to delete content"
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Restore content item from trash
|
|
699
|
+
*/
|
|
700
|
+
async function handleContentRestore(db, collection, id) {
|
|
701
|
+
try {
|
|
702
|
+
if (!await withTransaction(db, async (trx) => {
|
|
703
|
+
const repo = new ContentRepository(trx);
|
|
704
|
+
const resolvedId = await resolveIdIncludingTrashed(repo, collection, id) ?? id;
|
|
705
|
+
return repo.restore(collection, resolvedId);
|
|
706
|
+
})) return {
|
|
707
|
+
success: false,
|
|
708
|
+
error: {
|
|
709
|
+
code: "NOT_FOUND",
|
|
710
|
+
message: `Trashed content item not found: ${id}`
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
return {
|
|
714
|
+
success: true,
|
|
715
|
+
data: { restored: true }
|
|
716
|
+
};
|
|
717
|
+
} catch (error) {
|
|
718
|
+
console.error("Content restore error:", error);
|
|
719
|
+
return {
|
|
720
|
+
success: false,
|
|
721
|
+
error: {
|
|
722
|
+
code: "CONTENT_RESTORE_ERROR",
|
|
723
|
+
message: "Failed to restore content"
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Permanently delete content item (cannot be undone).
|
|
730
|
+
* Also cleans up associated SEO data.
|
|
731
|
+
*/
|
|
732
|
+
async function handleContentPermanentDelete(db, collection, id) {
|
|
733
|
+
try {
|
|
734
|
+
const resolvedId = await resolveIdIncludingTrashed(new ContentRepository(db), collection, id) ?? id;
|
|
735
|
+
if (!await withTransaction(db, async (trx) => {
|
|
736
|
+
const wasDeleted = await new ContentRepository(trx).permanentDelete(collection, resolvedId);
|
|
737
|
+
if (wasDeleted) {
|
|
738
|
+
await new SeoRepository(trx).delete(collection, resolvedId);
|
|
739
|
+
await new CommentRepository(trx).deleteByContent(collection, resolvedId);
|
|
740
|
+
await new RevisionRepository(trx).deleteByEntry(collection, resolvedId);
|
|
741
|
+
}
|
|
742
|
+
return wasDeleted;
|
|
743
|
+
})) return {
|
|
744
|
+
success: false,
|
|
745
|
+
error: {
|
|
746
|
+
code: "NOT_FOUND",
|
|
747
|
+
message: `Content item not found: ${id}`
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
return {
|
|
751
|
+
success: true,
|
|
752
|
+
data: { deleted: true }
|
|
753
|
+
};
|
|
754
|
+
} catch (error) {
|
|
755
|
+
console.error("Content permanent delete error:", error);
|
|
756
|
+
return {
|
|
757
|
+
success: false,
|
|
758
|
+
error: {
|
|
759
|
+
code: "CONTENT_DELETE_ERROR",
|
|
760
|
+
message: "Failed to permanently delete content"
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* List trashed content items
|
|
767
|
+
*/
|
|
768
|
+
async function handleContentListTrashed(db, collection, options = {}) {
|
|
769
|
+
try {
|
|
770
|
+
const result = await new ContentRepository(db).findTrashed(collection, {
|
|
771
|
+
limit: options.limit,
|
|
772
|
+
cursor: options.cursor
|
|
773
|
+
});
|
|
774
|
+
return {
|
|
775
|
+
success: true,
|
|
776
|
+
data: {
|
|
777
|
+
items: result.items.map((item) => ({
|
|
778
|
+
id: item.id,
|
|
779
|
+
type: item.type,
|
|
780
|
+
slug: item.slug,
|
|
781
|
+
status: item.status,
|
|
782
|
+
data: item.data,
|
|
783
|
+
authorId: item.authorId,
|
|
784
|
+
createdAt: item.createdAt,
|
|
785
|
+
updatedAt: item.updatedAt,
|
|
786
|
+
publishedAt: item.publishedAt,
|
|
787
|
+
deletedAt: item.deletedAt
|
|
788
|
+
})),
|
|
789
|
+
nextCursor: result.nextCursor
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
} catch (error) {
|
|
793
|
+
if (error instanceof InvalidCursorError) return {
|
|
794
|
+
success: false,
|
|
795
|
+
error: {
|
|
796
|
+
code: "INVALID_CURSOR",
|
|
797
|
+
message: error.message
|
|
798
|
+
}
|
|
799
|
+
};
|
|
800
|
+
console.error("Content list trashed error:", error);
|
|
801
|
+
return {
|
|
802
|
+
success: false,
|
|
803
|
+
error: {
|
|
804
|
+
code: "CONTENT_LIST_ERROR",
|
|
805
|
+
message: "Failed to list trashed content"
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Count trashed content items
|
|
812
|
+
*/
|
|
813
|
+
async function handleContentCountTrashed(db, collection) {
|
|
814
|
+
try {
|
|
815
|
+
return {
|
|
816
|
+
success: true,
|
|
817
|
+
data: { count: await new ContentRepository(db).countTrashed(collection) }
|
|
818
|
+
};
|
|
819
|
+
} catch (error) {
|
|
820
|
+
console.error("Content count trashed error:", error);
|
|
821
|
+
return {
|
|
822
|
+
success: false,
|
|
823
|
+
error: {
|
|
824
|
+
code: "CONTENT_COUNT_ERROR",
|
|
825
|
+
message: "Failed to count trashed content"
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Schedule content for future publishing
|
|
832
|
+
*/
|
|
833
|
+
async function handleContentSchedule(db, collection, id, scheduledAt) {
|
|
834
|
+
try {
|
|
835
|
+
const item = await withTransaction(db, async (trx) => {
|
|
836
|
+
const repo = new ContentRepository(trx);
|
|
837
|
+
const resolvedId = await resolveId(repo, collection, id) ?? id;
|
|
838
|
+
return repo.schedule(collection, resolvedId, scheduledAt);
|
|
839
|
+
});
|
|
840
|
+
await hydrateSeo(db, collection, item, await collectionHasSeo(db, collection));
|
|
841
|
+
return {
|
|
842
|
+
success: true,
|
|
843
|
+
data: { item }
|
|
844
|
+
};
|
|
845
|
+
} catch (error) {
|
|
846
|
+
if (error instanceof EmDashValidationError) return {
|
|
847
|
+
success: false,
|
|
848
|
+
error: {
|
|
849
|
+
code: "VALIDATION_ERROR",
|
|
850
|
+
message: error.message
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
console.error("Content schedule error:", error);
|
|
854
|
+
return {
|
|
855
|
+
success: false,
|
|
856
|
+
error: {
|
|
857
|
+
code: "CONTENT_SCHEDULE_ERROR",
|
|
858
|
+
message: "Failed to schedule content"
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Unschedule content (revert to draft)
|
|
865
|
+
*/
|
|
866
|
+
async function handleContentUnschedule(db, collection, id) {
|
|
867
|
+
try {
|
|
868
|
+
const item = await withTransaction(db, async (trx) => {
|
|
869
|
+
const repo = new ContentRepository(trx);
|
|
870
|
+
const resolvedId = await resolveId(repo, collection, id) ?? id;
|
|
871
|
+
return repo.unschedule(collection, resolvedId);
|
|
872
|
+
});
|
|
873
|
+
await hydrateSeo(db, collection, item, await collectionHasSeo(db, collection));
|
|
874
|
+
return {
|
|
875
|
+
success: true,
|
|
876
|
+
data: { item }
|
|
877
|
+
};
|
|
878
|
+
} catch (error) {
|
|
879
|
+
if (error instanceof EmDashValidationError) return {
|
|
880
|
+
success: false,
|
|
881
|
+
error: {
|
|
882
|
+
code: "VALIDATION_ERROR",
|
|
883
|
+
message: error.message
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
console.error("Content unschedule error:", error);
|
|
887
|
+
return {
|
|
888
|
+
success: false,
|
|
889
|
+
error: {
|
|
890
|
+
code: "CONTENT_UNSCHEDULE_ERROR",
|
|
891
|
+
message: "Failed to unschedule content"
|
|
892
|
+
}
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Publish content immediately.
|
|
898
|
+
*
|
|
899
|
+
* Wrapped in a transaction because publish performs multiple writes
|
|
900
|
+
* (syncDataColumns, slug sync, status/revision update) that must
|
|
901
|
+
* be atomic to prevent FTS shadow table corruption on crash.
|
|
902
|
+
*/
|
|
903
|
+
async function handleContentPublish(db, collection, id, options = {}) {
|
|
904
|
+
try {
|
|
905
|
+
const item = await withTransaction(db, async (trx) => {
|
|
906
|
+
const repo = new ContentRepository(trx);
|
|
907
|
+
const resolvedId = await resolveId(repo, collection, id) ?? id;
|
|
908
|
+
return repo.publish(collection, resolvedId, options.publishedAt);
|
|
909
|
+
});
|
|
910
|
+
await hydrateSeo(db, collection, item, await collectionHasSeo(db, collection));
|
|
911
|
+
return {
|
|
912
|
+
success: true,
|
|
913
|
+
data: { item }
|
|
914
|
+
};
|
|
915
|
+
} catch (error) {
|
|
916
|
+
if (error instanceof EmDashValidationError) return {
|
|
917
|
+
success: false,
|
|
918
|
+
error: {
|
|
919
|
+
code: "VALIDATION_ERROR",
|
|
920
|
+
message: error.message
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
console.error("Content publish error:", error);
|
|
924
|
+
return {
|
|
925
|
+
success: false,
|
|
926
|
+
error: {
|
|
927
|
+
code: "CONTENT_PUBLISH_ERROR",
|
|
928
|
+
message: "Failed to publish content"
|
|
929
|
+
}
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Unpublish content (revert to draft).
|
|
935
|
+
*
|
|
936
|
+
* Wrapped in a transaction — unpublish may create a draft revision
|
|
937
|
+
* from the live version then update the status, which is multi-step.
|
|
938
|
+
*/
|
|
939
|
+
async function handleContentUnpublish(db, collection, id) {
|
|
940
|
+
try {
|
|
941
|
+
const item = await withTransaction(db, async (trx) => {
|
|
942
|
+
const repo = new ContentRepository(trx);
|
|
943
|
+
const resolvedId = await resolveId(repo, collection, id) ?? id;
|
|
944
|
+
return repo.unpublish(collection, resolvedId);
|
|
945
|
+
});
|
|
946
|
+
await hydrateSeo(db, collection, item, await collectionHasSeo(db, collection));
|
|
947
|
+
return {
|
|
948
|
+
success: true,
|
|
949
|
+
data: { item }
|
|
950
|
+
};
|
|
951
|
+
} catch (error) {
|
|
952
|
+
if (error instanceof EmDashValidationError) return {
|
|
953
|
+
success: false,
|
|
954
|
+
error: {
|
|
955
|
+
code: "VALIDATION_ERROR",
|
|
956
|
+
message: error.message
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
console.error("Content unpublish error:", error);
|
|
960
|
+
return {
|
|
961
|
+
success: false,
|
|
962
|
+
error: {
|
|
963
|
+
code: "CONTENT_UNPUBLISH_ERROR",
|
|
964
|
+
message: "Failed to unpublish content"
|
|
965
|
+
}
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Count scheduled content items
|
|
971
|
+
*/
|
|
972
|
+
async function handleContentCountScheduled(db, collection) {
|
|
973
|
+
try {
|
|
974
|
+
return {
|
|
975
|
+
success: true,
|
|
976
|
+
data: { count: await new ContentRepository(db).countScheduled(collection) }
|
|
977
|
+
};
|
|
978
|
+
} catch (error) {
|
|
979
|
+
console.error("Content count scheduled error:", error);
|
|
980
|
+
return {
|
|
981
|
+
success: false,
|
|
982
|
+
error: {
|
|
983
|
+
code: "CONTENT_COUNT_ERROR",
|
|
984
|
+
message: "Failed to count scheduled content"
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Discard draft changes (revert to live version)
|
|
991
|
+
*/
|
|
992
|
+
async function handleContentDiscardDraft(db, collection, id) {
|
|
993
|
+
try {
|
|
994
|
+
const item = await withTransaction(db, async (trx) => {
|
|
995
|
+
const repo = new ContentRepository(trx);
|
|
996
|
+
const resolvedId = await resolveId(repo, collection, id) ?? id;
|
|
997
|
+
return repo.discardDraft(collection, resolvedId);
|
|
998
|
+
});
|
|
999
|
+
await hydrateSeo(db, collection, item, await collectionHasSeo(db, collection));
|
|
1000
|
+
return {
|
|
1001
|
+
success: true,
|
|
1002
|
+
data: { item }
|
|
1003
|
+
};
|
|
1004
|
+
} catch (error) {
|
|
1005
|
+
if (error instanceof EmDashValidationError) return {
|
|
1006
|
+
success: false,
|
|
1007
|
+
error: {
|
|
1008
|
+
code: "NOT_FOUND",
|
|
1009
|
+
message: error.message
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
1012
|
+
console.error("Content discard draft error:", error);
|
|
1013
|
+
return {
|
|
1014
|
+
success: false,
|
|
1015
|
+
error: {
|
|
1016
|
+
code: "CONTENT_DISCARD_DRAFT_ERROR",
|
|
1017
|
+
message: "Failed to discard draft"
|
|
1018
|
+
}
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Compare live and draft revisions
|
|
1024
|
+
*/
|
|
1025
|
+
async function handleContentCompare(db, collection, id) {
|
|
1026
|
+
try {
|
|
1027
|
+
const entry = await new ContentRepository(db).findByIdOrSlug(collection, id);
|
|
1028
|
+
if (!entry) return {
|
|
1029
|
+
success: false,
|
|
1030
|
+
error: {
|
|
1031
|
+
code: "NOT_FOUND",
|
|
1032
|
+
message: `Content item not found: ${id}`
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
const revisionRepo = new RevisionRepository(db);
|
|
1036
|
+
const live = entry.liveRevisionId ? await revisionRepo.findById(entry.liveRevisionId) : null;
|
|
1037
|
+
const draft = entry.draftRevisionId ? await revisionRepo.findById(entry.draftRevisionId) : null;
|
|
1038
|
+
return {
|
|
1039
|
+
success: true,
|
|
1040
|
+
data: {
|
|
1041
|
+
hasChanges: entry.draftRevisionId !== null && entry.draftRevisionId !== entry.liveRevisionId,
|
|
1042
|
+
live: live?.data ?? null,
|
|
1043
|
+
draft: draft?.data ?? null
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
} catch (error) {
|
|
1047
|
+
console.error("Content compare error:", error);
|
|
1048
|
+
return {
|
|
1049
|
+
success: false,
|
|
1050
|
+
error: {
|
|
1051
|
+
code: "CONTENT_COMPARE_ERROR",
|
|
1052
|
+
message: "Failed to compare revisions"
|
|
1053
|
+
}
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1058
|
+
* Get all translations for a content item.
|
|
1059
|
+
* Returns the item's translation group members with locale and status info.
|
|
1060
|
+
*/
|
|
1061
|
+
async function handleContentTranslations(db, collection, id) {
|
|
1062
|
+
try {
|
|
1063
|
+
const repo = new ContentRepository(db);
|
|
1064
|
+
const item = await repo.findByIdOrSlug(collection, id);
|
|
1065
|
+
if (!item) return {
|
|
1066
|
+
success: false,
|
|
1067
|
+
error: {
|
|
1068
|
+
code: "NOT_FOUND",
|
|
1069
|
+
message: `Content item not found: ${id}`
|
|
1070
|
+
}
|
|
1071
|
+
};
|
|
1072
|
+
if (!item.translationGroup) return {
|
|
1073
|
+
success: true,
|
|
1074
|
+
data: {
|
|
1075
|
+
translationGroup: item.id,
|
|
1076
|
+
translations: [{
|
|
1077
|
+
id: item.id,
|
|
1078
|
+
locale: item.locale,
|
|
1079
|
+
slug: item.slug,
|
|
1080
|
+
status: item.status,
|
|
1081
|
+
updatedAt: item.updatedAt
|
|
1082
|
+
}]
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
const translations = await repo.findTranslations(collection, item.translationGroup);
|
|
1086
|
+
return {
|
|
1087
|
+
success: true,
|
|
1088
|
+
data: {
|
|
1089
|
+
translationGroup: item.translationGroup,
|
|
1090
|
+
translations: translations.map((t) => ({
|
|
1091
|
+
id: t.id,
|
|
1092
|
+
locale: t.locale,
|
|
1093
|
+
slug: t.slug,
|
|
1094
|
+
status: t.status,
|
|
1095
|
+
updatedAt: t.updatedAt
|
|
1096
|
+
}))
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
} catch (error) {
|
|
1100
|
+
if (error instanceof Error) console.error("Content translations error:", error);
|
|
1101
|
+
return {
|
|
1102
|
+
success: false,
|
|
1103
|
+
error: {
|
|
1104
|
+
code: "CONTENT_TRANSLATIONS_ERROR",
|
|
1105
|
+
message: "Failed to get translations"
|
|
1106
|
+
}
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Sync non-translatable fields to sibling locales.
|
|
1112
|
+
*
|
|
1113
|
+
* When a content item is updated and it belongs to a translation group,
|
|
1114
|
+
* any non-translatable fields in the update data are written to all other
|
|
1115
|
+
* rows in the same translation group within the same transaction.
|
|
1116
|
+
*
|
|
1117
|
+
* Non-translatable fields are **copied, not linked** — each row owns its
|
|
1118
|
+
* own data. This keeps queries simple and avoids cross-row joins.
|
|
1119
|
+
*/
|
|
1120
|
+
async function syncNonTranslatableFields(trx, collectionSlug, updatedItemId, translationGroup, data) {
|
|
1121
|
+
const collection = await trx.selectFrom("_emdash_collections").select("id").where("slug", "=", collectionSlug).executeTakeFirst();
|
|
1122
|
+
if (!collection) return;
|
|
1123
|
+
const nonTranslatableSlugs = (await trx.selectFrom("_emdash_fields").select("slug").where("collection_id", "=", collection.id).where("translatable", "=", 0).execute()).map((f) => f.slug);
|
|
1124
|
+
if (nonTranslatableSlugs.length === 0) return;
|
|
1125
|
+
const syncData = {};
|
|
1126
|
+
for (const slug of nonTranslatableSlugs) if (slug in data) syncData[slug] = data[slug];
|
|
1127
|
+
if (Object.keys(syncData).length === 0) return;
|
|
1128
|
+
validateIdentifier(collectionSlug, "collection slug");
|
|
1129
|
+
const tableName = `ec_${collectionSlug}`;
|
|
1130
|
+
const setClauses = Object.entries(syncData).map(([key, value]) => {
|
|
1131
|
+
validateIdentifier(key, "field slug");
|
|
1132
|
+
const serialized = typeof value === "object" && value !== null ? JSON.stringify(value) : value;
|
|
1133
|
+
return sql`${sql.ref(key)} = ${serialized}`;
|
|
1134
|
+
});
|
|
1135
|
+
await sql`
|
|
1136
|
+
UPDATE ${sql.ref(tableName)}
|
|
1137
|
+
SET ${sql.join(setClauses, sql`, `)}
|
|
1138
|
+
WHERE translation_group = ${translationGroup}
|
|
1139
|
+
AND id != ${updatedItemId}
|
|
1140
|
+
`.execute(trx);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
//#endregion
|
|
1144
|
+
//#region src/api/handlers/manifest.ts
|
|
1145
|
+
/**
|
|
1146
|
+
* Manifest generation handlers
|
|
1147
|
+
*/
|
|
1148
|
+
/** Pattern to add spaces before capital letters */
|
|
1149
|
+
const CAMEL_CASE_PATTERN = /([A-Z])/g;
|
|
1150
|
+
const FIRST_CHAR_PATTERN = /^./;
|
|
1151
|
+
/**
|
|
1152
|
+
* Generate admin manifest from collections
|
|
1153
|
+
*/
|
|
1154
|
+
async function generateManifest(collections, plugins = {}) {
|
|
1155
|
+
const manifestCollections = {};
|
|
1156
|
+
for (const [name, definition] of Object.entries(collections)) {
|
|
1157
|
+
const fields = extractFieldDescriptors(definition.schema);
|
|
1158
|
+
manifestCollections[name] = {
|
|
1159
|
+
label: definition.admin.label,
|
|
1160
|
+
labelSingular: definition.admin.labelSingular || definition.admin.label,
|
|
1161
|
+
supports: definition.admin.supports || [],
|
|
1162
|
+
fields
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
return {
|
|
1166
|
+
version: "0.1.0",
|
|
1167
|
+
hash: await hashString(JSON.stringify(manifestCollections)),
|
|
1168
|
+
collections: manifestCollections,
|
|
1169
|
+
plugins
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Extract field descriptors from Zod schema
|
|
1174
|
+
* Note: This is a simplified implementation that handles common types
|
|
1175
|
+
*/
|
|
1176
|
+
function extractFieldDescriptors(schema) {
|
|
1177
|
+
const fields = {};
|
|
1178
|
+
const shape = typeof schema._def?.shape === "function" ? schema._def.shape() : schema.shape || {};
|
|
1179
|
+
for (const [name, fieldSchema] of Object.entries(shape)) fields[name] = extractFieldType(name, fieldSchema);
|
|
1180
|
+
return fields;
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Extract field type from Zod schema
|
|
1184
|
+
*/
|
|
1185
|
+
/** Type guard: check if a value is a non-null object */
|
|
1186
|
+
function isObject(value) {
|
|
1187
|
+
return typeof value === "object" && value !== null;
|
|
1188
|
+
}
|
|
1189
|
+
function extractFieldType(name, schema) {
|
|
1190
|
+
if (!isObject(schema)) return {
|
|
1191
|
+
kind: "string",
|
|
1192
|
+
label: formatLabel(name)
|
|
1193
|
+
};
|
|
1194
|
+
if (schema.isPortableText) return {
|
|
1195
|
+
kind: "portableText",
|
|
1196
|
+
label: formatLabel(name)
|
|
1197
|
+
};
|
|
1198
|
+
if (schema.isImage) return {
|
|
1199
|
+
kind: "image",
|
|
1200
|
+
label: formatLabel(name)
|
|
1201
|
+
};
|
|
1202
|
+
if (schema.isReference) return {
|
|
1203
|
+
kind: "reference",
|
|
1204
|
+
label: formatLabel(name)
|
|
1205
|
+
};
|
|
1206
|
+
const def = isObject(schema._def) ? schema._def : void 0;
|
|
1207
|
+
switch (typeof def?.typeName === "string" ? def.typeName : void 0) {
|
|
1208
|
+
case "ZodString": return {
|
|
1209
|
+
kind: "string",
|
|
1210
|
+
label: formatLabel(name)
|
|
1211
|
+
};
|
|
1212
|
+
case "ZodNumber": return {
|
|
1213
|
+
kind: "number",
|
|
1214
|
+
label: formatLabel(name)
|
|
1215
|
+
};
|
|
1216
|
+
case "ZodBoolean": return {
|
|
1217
|
+
kind: "boolean",
|
|
1218
|
+
label: formatLabel(name)
|
|
1219
|
+
};
|
|
1220
|
+
case "ZodDate": return {
|
|
1221
|
+
kind: "datetime",
|
|
1222
|
+
label: formatLabel(name)
|
|
1223
|
+
};
|
|
1224
|
+
case "ZodEnum": {
|
|
1225
|
+
const values = Array.isArray(def?.values) ? def.values : [];
|
|
1226
|
+
return {
|
|
1227
|
+
kind: "select",
|
|
1228
|
+
label: formatLabel(name),
|
|
1229
|
+
options: values.filter((v) => typeof v === "string").map((v) => ({
|
|
1230
|
+
value: v,
|
|
1231
|
+
label: v.charAt(0).toUpperCase() + v.slice(1)
|
|
1232
|
+
}))
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
case "ZodArray": return {
|
|
1236
|
+
kind: "array",
|
|
1237
|
+
label: formatLabel(name)
|
|
1238
|
+
};
|
|
1239
|
+
case "ZodObject": return {
|
|
1240
|
+
kind: "object",
|
|
1241
|
+
label: formatLabel(name)
|
|
1242
|
+
};
|
|
1243
|
+
case "ZodOptional":
|
|
1244
|
+
case "ZodDefault":
|
|
1245
|
+
if (def?.innerType) return extractFieldType(name, def.innerType);
|
|
1246
|
+
return {
|
|
1247
|
+
kind: "string",
|
|
1248
|
+
label: formatLabel(name)
|
|
1249
|
+
};
|
|
1250
|
+
default: return {
|
|
1251
|
+
kind: "string",
|
|
1252
|
+
label: formatLabel(name)
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Format field name as label
|
|
1258
|
+
*/
|
|
1259
|
+
function formatLabel(name) {
|
|
1260
|
+
return name.replace(CAMEL_CASE_PATTERN, " $1").replace(FIRST_CHAR_PATTERN, (str) => str.toUpperCase()).trim();
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
//#endregion
|
|
1264
|
+
//#region src/api/handlers/revision.ts
|
|
1265
|
+
/**
|
|
1266
|
+
* List revisions for a content entry
|
|
1267
|
+
*/
|
|
1268
|
+
async function handleRevisionList(db, collection, entryId, params = {}) {
|
|
1269
|
+
try {
|
|
1270
|
+
const repo = new RevisionRepository(db);
|
|
1271
|
+
const [items, total] = await Promise.all([repo.findByEntry(collection, entryId, { limit: Math.min(params.limit || 50, 100) }), repo.countByEntry(collection, entryId)]);
|
|
1272
|
+
return {
|
|
1273
|
+
success: true,
|
|
1274
|
+
data: {
|
|
1275
|
+
items,
|
|
1276
|
+
total
|
|
1277
|
+
}
|
|
1278
|
+
};
|
|
1279
|
+
} catch {
|
|
1280
|
+
return {
|
|
1281
|
+
success: false,
|
|
1282
|
+
error: {
|
|
1283
|
+
code: "REVISION_LIST_ERROR",
|
|
1284
|
+
message: "Failed to list revisions"
|
|
1285
|
+
}
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* Get a specific revision
|
|
1291
|
+
*/
|
|
1292
|
+
async function handleRevisionGet(db, revisionId) {
|
|
1293
|
+
try {
|
|
1294
|
+
const item = await new RevisionRepository(db).findById(revisionId);
|
|
1295
|
+
if (!item) return {
|
|
1296
|
+
success: false,
|
|
1297
|
+
error: {
|
|
1298
|
+
code: "NOT_FOUND",
|
|
1299
|
+
message: `Revision not found: ${revisionId}`
|
|
1300
|
+
}
|
|
1301
|
+
};
|
|
1302
|
+
return {
|
|
1303
|
+
success: true,
|
|
1304
|
+
data: { item }
|
|
1305
|
+
};
|
|
1306
|
+
} catch {
|
|
1307
|
+
return {
|
|
1308
|
+
success: false,
|
|
1309
|
+
error: {
|
|
1310
|
+
code: "REVISION_GET_ERROR",
|
|
1311
|
+
message: "Failed to get revision"
|
|
1312
|
+
}
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Restore a revision (updates content to this revision's data and creates new revision)
|
|
1318
|
+
*/
|
|
1319
|
+
async function handleRevisionRestore(db, revisionId, callerUserId) {
|
|
1320
|
+
try {
|
|
1321
|
+
const revision = await new RevisionRepository(db).findById(revisionId);
|
|
1322
|
+
if (!revision) return {
|
|
1323
|
+
success: false,
|
|
1324
|
+
error: {
|
|
1325
|
+
code: "NOT_FOUND",
|
|
1326
|
+
message: `Revision not found: ${revisionId}`
|
|
1327
|
+
}
|
|
1328
|
+
};
|
|
1329
|
+
const { _slug, ...fieldData } = revision.data;
|
|
1330
|
+
const item = await withTransaction(db, async (trx) => {
|
|
1331
|
+
const trxContentRepo = new ContentRepository(trx);
|
|
1332
|
+
const trxRevisionRepo = new RevisionRepository(trx);
|
|
1333
|
+
const updated = await trxContentRepo.update(revision.collection, revision.entryId, {
|
|
1334
|
+
data: fieldData,
|
|
1335
|
+
slug: typeof _slug === "string" ? _slug : void 0
|
|
1336
|
+
});
|
|
1337
|
+
await trxRevisionRepo.create({
|
|
1338
|
+
collection: revision.collection,
|
|
1339
|
+
entryId: revision.entryId,
|
|
1340
|
+
data: revision.data,
|
|
1341
|
+
authorId: callerUserId
|
|
1342
|
+
});
|
|
1343
|
+
return updated;
|
|
1344
|
+
});
|
|
1345
|
+
new RevisionRepository(db).pruneOldRevisions(revision.collection, revision.entryId, 50).catch(() => {});
|
|
1346
|
+
return {
|
|
1347
|
+
success: true,
|
|
1348
|
+
data: { item }
|
|
1349
|
+
};
|
|
1350
|
+
} catch {
|
|
1351
|
+
return {
|
|
1352
|
+
success: false,
|
|
1353
|
+
error: {
|
|
1354
|
+
code: "REVISION_RESTORE_ERROR",
|
|
1355
|
+
message: "Failed to restore revision"
|
|
1356
|
+
}
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
//#endregion
|
|
1362
|
+
//#region src/api/handlers/media.ts
|
|
1363
|
+
/**
|
|
1364
|
+
* List media items
|
|
1365
|
+
*/
|
|
1366
|
+
async function handleMediaList(db, params) {
|
|
1367
|
+
try {
|
|
1368
|
+
const result = await new MediaRepository(db).findMany({
|
|
1369
|
+
cursor: params.cursor,
|
|
1370
|
+
limit: Math.min(params.limit || 50, 100),
|
|
1371
|
+
mimeType: params.mimeType
|
|
1372
|
+
});
|
|
1373
|
+
return {
|
|
1374
|
+
success: true,
|
|
1375
|
+
data: {
|
|
1376
|
+
items: result.items,
|
|
1377
|
+
nextCursor: result.nextCursor
|
|
1378
|
+
}
|
|
1379
|
+
};
|
|
1380
|
+
} catch (error) {
|
|
1381
|
+
if (error instanceof InvalidCursorError) return {
|
|
1382
|
+
success: false,
|
|
1383
|
+
error: {
|
|
1384
|
+
code: "INVALID_CURSOR",
|
|
1385
|
+
message: error.message
|
|
1386
|
+
}
|
|
1387
|
+
};
|
|
1388
|
+
return {
|
|
1389
|
+
success: false,
|
|
1390
|
+
error: {
|
|
1391
|
+
code: "MEDIA_LIST_ERROR",
|
|
1392
|
+
message: "Failed to list media"
|
|
1393
|
+
}
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
/**
|
|
1398
|
+
* Get single media item
|
|
1399
|
+
*/
|
|
1400
|
+
async function handleMediaGet(db, id) {
|
|
1401
|
+
try {
|
|
1402
|
+
const item = await new MediaRepository(db).findById(id);
|
|
1403
|
+
if (!item) return {
|
|
1404
|
+
success: false,
|
|
1405
|
+
error: {
|
|
1406
|
+
code: "NOT_FOUND",
|
|
1407
|
+
message: `Media item not found: ${id}`
|
|
1408
|
+
}
|
|
1409
|
+
};
|
|
1410
|
+
return {
|
|
1411
|
+
success: true,
|
|
1412
|
+
data: { item }
|
|
1413
|
+
};
|
|
1414
|
+
} catch {
|
|
1415
|
+
return {
|
|
1416
|
+
success: false,
|
|
1417
|
+
error: {
|
|
1418
|
+
code: "MEDIA_GET_ERROR",
|
|
1419
|
+
message: "Failed to get media"
|
|
1420
|
+
}
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
/**
|
|
1425
|
+
* Create media item (after file upload)
|
|
1426
|
+
*/
|
|
1427
|
+
async function handleMediaCreate(db, input) {
|
|
1428
|
+
try {
|
|
1429
|
+
return {
|
|
1430
|
+
success: true,
|
|
1431
|
+
data: { item: await new MediaRepository(db).create(input) }
|
|
1432
|
+
};
|
|
1433
|
+
} catch {
|
|
1434
|
+
return {
|
|
1435
|
+
success: false,
|
|
1436
|
+
error: {
|
|
1437
|
+
code: "MEDIA_CREATE_ERROR",
|
|
1438
|
+
message: "Failed to create media"
|
|
1439
|
+
}
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
/**
|
|
1444
|
+
* Update media metadata
|
|
1445
|
+
*/
|
|
1446
|
+
async function handleMediaUpdate(db, id, input) {
|
|
1447
|
+
try {
|
|
1448
|
+
const item = await new MediaRepository(db).update(id, input);
|
|
1449
|
+
if (!item) return {
|
|
1450
|
+
success: false,
|
|
1451
|
+
error: {
|
|
1452
|
+
code: "NOT_FOUND",
|
|
1453
|
+
message: `Media item not found: ${id}`
|
|
1454
|
+
}
|
|
1455
|
+
};
|
|
1456
|
+
return {
|
|
1457
|
+
success: true,
|
|
1458
|
+
data: { item }
|
|
1459
|
+
};
|
|
1460
|
+
} catch {
|
|
1461
|
+
return {
|
|
1462
|
+
success: false,
|
|
1463
|
+
error: {
|
|
1464
|
+
code: "MEDIA_UPDATE_ERROR",
|
|
1465
|
+
message: "Failed to update media"
|
|
1466
|
+
}
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
/**
|
|
1471
|
+
* Delete media item
|
|
1472
|
+
*/
|
|
1473
|
+
async function handleMediaDelete(db, id) {
|
|
1474
|
+
try {
|
|
1475
|
+
if (!await new MediaRepository(db).delete(id)) return {
|
|
1476
|
+
success: false,
|
|
1477
|
+
error: {
|
|
1478
|
+
code: "NOT_FOUND",
|
|
1479
|
+
message: `Media item not found: ${id}`
|
|
1480
|
+
}
|
|
1481
|
+
};
|
|
1482
|
+
return {
|
|
1483
|
+
success: true,
|
|
1484
|
+
data: { deleted: true }
|
|
1485
|
+
};
|
|
1486
|
+
} catch {
|
|
1487
|
+
return {
|
|
1488
|
+
success: false,
|
|
1489
|
+
error: {
|
|
1490
|
+
code: "MEDIA_DELETE_ERROR",
|
|
1491
|
+
message: "Failed to delete media"
|
|
1492
|
+
}
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
//#endregion
|
|
1498
|
+
//#region src/api/handlers/schema.ts
|
|
1499
|
+
/**
|
|
1500
|
+
* List all collections
|
|
1501
|
+
*/
|
|
1502
|
+
async function handleSchemaCollectionList(db) {
|
|
1503
|
+
try {
|
|
1504
|
+
return {
|
|
1505
|
+
success: true,
|
|
1506
|
+
data: { items: await new SchemaRegistry(db).listCollections() }
|
|
1507
|
+
};
|
|
1508
|
+
} catch {
|
|
1509
|
+
return {
|
|
1510
|
+
success: false,
|
|
1511
|
+
error: {
|
|
1512
|
+
code: "SCHEMA_LIST_ERROR",
|
|
1513
|
+
message: "Failed to list collections"
|
|
1514
|
+
}
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
/**
|
|
1519
|
+
* Get a collection by slug
|
|
1520
|
+
*/
|
|
1521
|
+
async function handleSchemaCollectionGet(db, slug, options) {
|
|
1522
|
+
try {
|
|
1523
|
+
const registry = new SchemaRegistry(db);
|
|
1524
|
+
if (options?.includeFields) {
|
|
1525
|
+
const item = await registry.getCollectionWithFields(slug);
|
|
1526
|
+
if (!item) return {
|
|
1527
|
+
success: false,
|
|
1528
|
+
error: {
|
|
1529
|
+
code: "NOT_FOUND",
|
|
1530
|
+
message: `Collection not found: ${slug}`
|
|
1531
|
+
}
|
|
1532
|
+
};
|
|
1533
|
+
return {
|
|
1534
|
+
success: true,
|
|
1535
|
+
data: { item }
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
const item = await registry.getCollection(slug);
|
|
1539
|
+
if (!item) return {
|
|
1540
|
+
success: false,
|
|
1541
|
+
error: {
|
|
1542
|
+
code: "NOT_FOUND",
|
|
1543
|
+
message: `Collection not found: ${slug}`
|
|
1544
|
+
}
|
|
1545
|
+
};
|
|
1546
|
+
return {
|
|
1547
|
+
success: true,
|
|
1548
|
+
data: { item }
|
|
1549
|
+
};
|
|
1550
|
+
} catch {
|
|
1551
|
+
return {
|
|
1552
|
+
success: false,
|
|
1553
|
+
error: {
|
|
1554
|
+
code: "SCHEMA_GET_ERROR",
|
|
1555
|
+
message: "Failed to get collection"
|
|
1556
|
+
}
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* Create a collection
|
|
1562
|
+
*/
|
|
1563
|
+
async function handleSchemaCollectionCreate(db, input) {
|
|
1564
|
+
try {
|
|
1565
|
+
return {
|
|
1566
|
+
success: true,
|
|
1567
|
+
data: { item: await new SchemaRegistry(db).createCollection(input) }
|
|
1568
|
+
};
|
|
1569
|
+
} catch (error) {
|
|
1570
|
+
if (error instanceof SchemaError) return {
|
|
1571
|
+
success: false,
|
|
1572
|
+
error: {
|
|
1573
|
+
code: error.code,
|
|
1574
|
+
message: error.message,
|
|
1575
|
+
details: error.details
|
|
1576
|
+
}
|
|
1577
|
+
};
|
|
1578
|
+
console.error("[emdash] Failed to create collection:", error);
|
|
1579
|
+
return {
|
|
1580
|
+
success: false,
|
|
1581
|
+
error: {
|
|
1582
|
+
code: "SCHEMA_CREATE_ERROR",
|
|
1583
|
+
message: "Failed to create collection"
|
|
1584
|
+
}
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
/**
|
|
1589
|
+
* Update a collection
|
|
1590
|
+
*/
|
|
1591
|
+
async function handleSchemaCollectionUpdate(db, slug, input) {
|
|
1592
|
+
try {
|
|
1593
|
+
return {
|
|
1594
|
+
success: true,
|
|
1595
|
+
data: { item: await new SchemaRegistry(db).updateCollection(slug, input) }
|
|
1596
|
+
};
|
|
1597
|
+
} catch (error) {
|
|
1598
|
+
if (error instanceof SchemaError) return {
|
|
1599
|
+
success: false,
|
|
1600
|
+
error: {
|
|
1601
|
+
code: error.code,
|
|
1602
|
+
message: error.message,
|
|
1603
|
+
details: error.details
|
|
1604
|
+
}
|
|
1605
|
+
};
|
|
1606
|
+
return {
|
|
1607
|
+
success: false,
|
|
1608
|
+
error: {
|
|
1609
|
+
code: "SCHEMA_UPDATE_ERROR",
|
|
1610
|
+
message: "Failed to update collection"
|
|
1611
|
+
}
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* Delete a collection
|
|
1617
|
+
*/
|
|
1618
|
+
async function handleSchemaCollectionDelete(db, slug, options) {
|
|
1619
|
+
try {
|
|
1620
|
+
await new SchemaRegistry(db).deleteCollection(slug, options);
|
|
1621
|
+
return {
|
|
1622
|
+
success: true,
|
|
1623
|
+
data: { success: true }
|
|
1624
|
+
};
|
|
1625
|
+
} catch (error) {
|
|
1626
|
+
if (error instanceof SchemaError) return {
|
|
1627
|
+
success: false,
|
|
1628
|
+
error: {
|
|
1629
|
+
code: error.code,
|
|
1630
|
+
message: error.message,
|
|
1631
|
+
details: error.details
|
|
1632
|
+
}
|
|
1633
|
+
};
|
|
1634
|
+
return {
|
|
1635
|
+
success: false,
|
|
1636
|
+
error: {
|
|
1637
|
+
code: "SCHEMA_DELETE_ERROR",
|
|
1638
|
+
message: "Failed to delete collection"
|
|
1639
|
+
}
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
/**
|
|
1644
|
+
* List fields for a collection
|
|
1645
|
+
*/
|
|
1646
|
+
async function handleSchemaFieldList(db, collectionSlug) {
|
|
1647
|
+
try {
|
|
1648
|
+
const registry = new SchemaRegistry(db);
|
|
1649
|
+
const collection = await registry.getCollection(collectionSlug);
|
|
1650
|
+
if (!collection) return {
|
|
1651
|
+
success: false,
|
|
1652
|
+
error: {
|
|
1653
|
+
code: "NOT_FOUND",
|
|
1654
|
+
message: `Collection not found: ${collectionSlug}`
|
|
1655
|
+
}
|
|
1656
|
+
};
|
|
1657
|
+
return {
|
|
1658
|
+
success: true,
|
|
1659
|
+
data: { items: await registry.listFields(collection.id) }
|
|
1660
|
+
};
|
|
1661
|
+
} catch {
|
|
1662
|
+
return {
|
|
1663
|
+
success: false,
|
|
1664
|
+
error: {
|
|
1665
|
+
code: "SCHEMA_FIELD_LIST_ERROR",
|
|
1666
|
+
message: "Failed to list fields"
|
|
1667
|
+
}
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
/**
|
|
1672
|
+
* Get a field
|
|
1673
|
+
*/
|
|
1674
|
+
async function handleSchemaFieldGet(db, collectionSlug, fieldSlug) {
|
|
1675
|
+
try {
|
|
1676
|
+
const item = await new SchemaRegistry(db).getField(collectionSlug, fieldSlug);
|
|
1677
|
+
if (!item) return {
|
|
1678
|
+
success: false,
|
|
1679
|
+
error: {
|
|
1680
|
+
code: "NOT_FOUND",
|
|
1681
|
+
message: `Field not found: ${fieldSlug} in collection ${collectionSlug}`
|
|
1682
|
+
}
|
|
1683
|
+
};
|
|
1684
|
+
return {
|
|
1685
|
+
success: true,
|
|
1686
|
+
data: { item }
|
|
1687
|
+
};
|
|
1688
|
+
} catch {
|
|
1689
|
+
return {
|
|
1690
|
+
success: false,
|
|
1691
|
+
error: {
|
|
1692
|
+
code: "SCHEMA_FIELD_GET_ERROR",
|
|
1693
|
+
message: "Failed to get field"
|
|
1694
|
+
}
|
|
1695
|
+
};
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
/**
|
|
1699
|
+
* Create a field
|
|
1700
|
+
*/
|
|
1701
|
+
async function handleSchemaFieldCreate(db, collectionSlug, input) {
|
|
1702
|
+
try {
|
|
1703
|
+
return {
|
|
1704
|
+
success: true,
|
|
1705
|
+
data: { item: await new SchemaRegistry(db).createField(collectionSlug, input) }
|
|
1706
|
+
};
|
|
1707
|
+
} catch (error) {
|
|
1708
|
+
if (error instanceof SchemaError) return {
|
|
1709
|
+
success: false,
|
|
1710
|
+
error: {
|
|
1711
|
+
code: error.code,
|
|
1712
|
+
message: error.message,
|
|
1713
|
+
details: error.details
|
|
1714
|
+
}
|
|
1715
|
+
};
|
|
1716
|
+
return {
|
|
1717
|
+
success: false,
|
|
1718
|
+
error: {
|
|
1719
|
+
code: "SCHEMA_FIELD_CREATE_ERROR",
|
|
1720
|
+
message: "Failed to create field"
|
|
1721
|
+
}
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
/**
|
|
1726
|
+
* Update a field
|
|
1727
|
+
*/
|
|
1728
|
+
async function handleSchemaFieldUpdate(db, collectionSlug, fieldSlug, input) {
|
|
1729
|
+
try {
|
|
1730
|
+
return {
|
|
1731
|
+
success: true,
|
|
1732
|
+
data: { item: await new SchemaRegistry(db).updateField(collectionSlug, fieldSlug, input) }
|
|
1733
|
+
};
|
|
1734
|
+
} catch (error) {
|
|
1735
|
+
if (error instanceof SchemaError) return {
|
|
1736
|
+
success: false,
|
|
1737
|
+
error: {
|
|
1738
|
+
code: error.code,
|
|
1739
|
+
message: error.message,
|
|
1740
|
+
details: error.details
|
|
1741
|
+
}
|
|
1742
|
+
};
|
|
1743
|
+
return {
|
|
1744
|
+
success: false,
|
|
1745
|
+
error: {
|
|
1746
|
+
code: "SCHEMA_FIELD_UPDATE_ERROR",
|
|
1747
|
+
message: "Failed to update field"
|
|
1748
|
+
}
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
/**
|
|
1753
|
+
* Delete a field
|
|
1754
|
+
*/
|
|
1755
|
+
async function handleSchemaFieldDelete(db, collectionSlug, fieldSlug) {
|
|
1756
|
+
try {
|
|
1757
|
+
await new SchemaRegistry(db).deleteField(collectionSlug, fieldSlug);
|
|
1758
|
+
return {
|
|
1759
|
+
success: true,
|
|
1760
|
+
data: { success: true }
|
|
1761
|
+
};
|
|
1762
|
+
} catch (error) {
|
|
1763
|
+
if (error instanceof SchemaError) return {
|
|
1764
|
+
success: false,
|
|
1765
|
+
error: {
|
|
1766
|
+
code: error.code,
|
|
1767
|
+
message: error.message,
|
|
1768
|
+
details: error.details
|
|
1769
|
+
}
|
|
1770
|
+
};
|
|
1771
|
+
return {
|
|
1772
|
+
success: false,
|
|
1773
|
+
error: {
|
|
1774
|
+
code: "SCHEMA_FIELD_DELETE_ERROR",
|
|
1775
|
+
message: "Failed to delete field"
|
|
1776
|
+
}
|
|
1777
|
+
};
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
/**
|
|
1781
|
+
* Reorder fields
|
|
1782
|
+
*/
|
|
1783
|
+
async function handleSchemaFieldReorder(db, collectionSlug, fieldSlugs) {
|
|
1784
|
+
try {
|
|
1785
|
+
await new SchemaRegistry(db).reorderFields(collectionSlug, fieldSlugs);
|
|
1786
|
+
return {
|
|
1787
|
+
success: true,
|
|
1788
|
+
data: { success: true }
|
|
1789
|
+
};
|
|
1790
|
+
} catch (error) {
|
|
1791
|
+
if (error instanceof SchemaError) return {
|
|
1792
|
+
success: false,
|
|
1793
|
+
error: {
|
|
1794
|
+
code: error.code,
|
|
1795
|
+
message: error.message,
|
|
1796
|
+
details: error.details
|
|
1797
|
+
}
|
|
1798
|
+
};
|
|
1799
|
+
return {
|
|
1800
|
+
success: false,
|
|
1801
|
+
error: {
|
|
1802
|
+
code: "SCHEMA_FIELD_REORDER_ERROR",
|
|
1803
|
+
message: "Failed to reorder fields"
|
|
1804
|
+
}
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
/**
|
|
1809
|
+
* List orphaned content tables
|
|
1810
|
+
*/
|
|
1811
|
+
async function handleOrphanedTableList(db) {
|
|
1812
|
+
try {
|
|
1813
|
+
return {
|
|
1814
|
+
success: true,
|
|
1815
|
+
data: { items: await new SchemaRegistry(db).discoverOrphanedTables() }
|
|
1816
|
+
};
|
|
1817
|
+
} catch (error) {
|
|
1818
|
+
console.error("[emdash] Failed to list orphaned tables:", error);
|
|
1819
|
+
return {
|
|
1820
|
+
success: false,
|
|
1821
|
+
error: {
|
|
1822
|
+
code: "ORPHAN_LIST_ERROR",
|
|
1823
|
+
message: "Failed to list orphaned tables"
|
|
1824
|
+
}
|
|
1825
|
+
};
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
/**
|
|
1829
|
+
* Register an orphaned table as a collection
|
|
1830
|
+
*/
|
|
1831
|
+
async function handleOrphanedTableRegister(db, slug, options) {
|
|
1832
|
+
try {
|
|
1833
|
+
return {
|
|
1834
|
+
success: true,
|
|
1835
|
+
data: { item: await new SchemaRegistry(db).registerOrphanedTable(slug, options) }
|
|
1836
|
+
};
|
|
1837
|
+
} catch (error) {
|
|
1838
|
+
if (error instanceof SchemaError) return {
|
|
1839
|
+
success: false,
|
|
1840
|
+
error: {
|
|
1841
|
+
code: error.code,
|
|
1842
|
+
message: error.message,
|
|
1843
|
+
details: error.details
|
|
1844
|
+
}
|
|
1845
|
+
};
|
|
1846
|
+
return {
|
|
1847
|
+
success: false,
|
|
1848
|
+
error: {
|
|
1849
|
+
code: "ORPHAN_REGISTER_ERROR",
|
|
1850
|
+
message: "Failed to register orphaned table"
|
|
1851
|
+
}
|
|
1852
|
+
};
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
//#endregion
|
|
1857
|
+
//#region src/plugins/state.ts
|
|
1858
|
+
function toPluginStatus(value) {
|
|
1859
|
+
if (value === "active") return "active";
|
|
1860
|
+
return "inactive";
|
|
1861
|
+
}
|
|
1862
|
+
function toPluginSource(value) {
|
|
1863
|
+
if (value === "marketplace") return "marketplace";
|
|
1864
|
+
if (value === "registry") return "registry";
|
|
1865
|
+
return "config";
|
|
1866
|
+
}
|
|
1867
|
+
/**
|
|
1868
|
+
* Repository for plugin state in the database
|
|
1869
|
+
*/
|
|
1870
|
+
var PluginStateRepository = class {
|
|
1871
|
+
constructor(db) {
|
|
1872
|
+
this.db = db;
|
|
1873
|
+
}
|
|
1874
|
+
/**
|
|
1875
|
+
* Get state for a specific plugin
|
|
1876
|
+
*/
|
|
1877
|
+
async get(pluginId) {
|
|
1878
|
+
const row = await this.db.selectFrom("_plugin_state").selectAll().where("plugin_id", "=", pluginId).executeTakeFirst();
|
|
1879
|
+
if (!row) return null;
|
|
1880
|
+
return rowToPluginState(row);
|
|
1881
|
+
}
|
|
1882
|
+
/**
|
|
1883
|
+
* Get all plugin states
|
|
1884
|
+
*/
|
|
1885
|
+
async getAll() {
|
|
1886
|
+
return (await this.db.selectFrom("_plugin_state").selectAll().execute()).map(rowToPluginState);
|
|
1887
|
+
}
|
|
1888
|
+
/**
|
|
1889
|
+
* Get all marketplace-installed plugin states
|
|
1890
|
+
*/
|
|
1891
|
+
async getMarketplacePlugins() {
|
|
1892
|
+
return (await this.db.selectFrom("_plugin_state").selectAll().where("source", "=", "marketplace").execute()).map(rowToPluginState);
|
|
1893
|
+
}
|
|
1894
|
+
/**
|
|
1895
|
+
* Get all registry-installed plugin states.
|
|
1896
|
+
*
|
|
1897
|
+
* The runtime's registry sync path uses this to discover which
|
|
1898
|
+
* registry plugins should be loaded into the sandbox on this worker.
|
|
1899
|
+
*/
|
|
1900
|
+
async getRegistryPlugins() {
|
|
1901
|
+
return (await this.db.selectFrom("_plugin_state").selectAll().where("source", "=", "registry").execute()).map(rowToPluginState);
|
|
1902
|
+
}
|
|
1903
|
+
/**
|
|
1904
|
+
* Create or update plugin state
|
|
1905
|
+
*/
|
|
1906
|
+
async upsert(pluginId, version, status, opts) {
|
|
1907
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1908
|
+
const existing = await this.get(pluginId);
|
|
1909
|
+
if (existing) {
|
|
1910
|
+
const updates = {
|
|
1911
|
+
status,
|
|
1912
|
+
version
|
|
1913
|
+
};
|
|
1914
|
+
if (status === "active" && existing.status !== "active") updates.activated_at = now;
|
|
1915
|
+
else if (status === "inactive" && existing.status !== "inactive") updates.deactivated_at = now;
|
|
1916
|
+
if (opts?.source) updates.source = opts.source;
|
|
1917
|
+
if (opts?.marketplaceVersion !== void 0) updates.marketplace_version = opts.marketplaceVersion;
|
|
1918
|
+
if (opts?.displayName !== void 0) updates.display_name = opts.displayName;
|
|
1919
|
+
if (opts?.description !== void 0) updates.description = opts.description;
|
|
1920
|
+
if (opts?.registryPublisherDid !== void 0) updates.registry_publisher_did = opts.registryPublisherDid;
|
|
1921
|
+
if (opts?.registrySlug !== void 0) updates.registry_slug = opts.registrySlug;
|
|
1922
|
+
await this.db.updateTable("_plugin_state").set(updates).where("plugin_id", "=", pluginId).execute();
|
|
1923
|
+
} else await this.db.insertInto("_plugin_state").values({
|
|
1924
|
+
plugin_id: pluginId,
|
|
1925
|
+
status,
|
|
1926
|
+
version,
|
|
1927
|
+
installed_at: now,
|
|
1928
|
+
activated_at: status === "active" ? now : null,
|
|
1929
|
+
deactivated_at: null,
|
|
1930
|
+
data: null,
|
|
1931
|
+
source: opts?.source ?? "config",
|
|
1932
|
+
marketplace_version: opts?.marketplaceVersion ?? null,
|
|
1933
|
+
display_name: opts?.displayName ?? null,
|
|
1934
|
+
description: opts?.description ?? null,
|
|
1935
|
+
registry_publisher_did: opts?.registryPublisherDid ?? null,
|
|
1936
|
+
registry_slug: opts?.registrySlug ?? null
|
|
1937
|
+
}).execute();
|
|
1938
|
+
return await this.get(pluginId);
|
|
1939
|
+
}
|
|
1940
|
+
/**
|
|
1941
|
+
* Enable a plugin
|
|
1942
|
+
*/
|
|
1943
|
+
async enable(pluginId, version) {
|
|
1944
|
+
return this.upsert(pluginId, version, "active");
|
|
1945
|
+
}
|
|
1946
|
+
/**
|
|
1947
|
+
* Disable a plugin
|
|
1948
|
+
*/
|
|
1949
|
+
async disable(pluginId, version) {
|
|
1950
|
+
return this.upsert(pluginId, version, "inactive");
|
|
1951
|
+
}
|
|
1952
|
+
/**
|
|
1953
|
+
* Delete plugin state
|
|
1954
|
+
*/
|
|
1955
|
+
async delete(pluginId) {
|
|
1956
|
+
return ((await this.db.deleteFrom("_plugin_state").where("plugin_id", "=", pluginId).executeTakeFirst()).numDeletedRows ?? 0) > 0;
|
|
1957
|
+
}
|
|
1958
|
+
};
|
|
1959
|
+
function rowToPluginState(row) {
|
|
1960
|
+
return {
|
|
1961
|
+
pluginId: row.plugin_id,
|
|
1962
|
+
status: toPluginStatus(row.status),
|
|
1963
|
+
version: row.version,
|
|
1964
|
+
installedAt: new Date(row.installed_at),
|
|
1965
|
+
activatedAt: row.activated_at ? new Date(row.activated_at) : null,
|
|
1966
|
+
deactivatedAt: row.deactivated_at ? new Date(row.deactivated_at) : null,
|
|
1967
|
+
source: toPluginSource(row.source),
|
|
1968
|
+
marketplaceVersion: row.marketplace_version ?? null,
|
|
1969
|
+
displayName: row.display_name ?? null,
|
|
1970
|
+
description: row.description ?? null,
|
|
1971
|
+
registryPublisherDid: row.registry_publisher_did ?? null,
|
|
1972
|
+
registrySlug: row.registry_slug ?? null
|
|
1973
|
+
};
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
//#endregion
|
|
1977
|
+
//#region src/api/handlers/plugins.ts
|
|
1978
|
+
function marketplaceIconUrl(marketplaceUrl, pluginId) {
|
|
1979
|
+
return `${marketplaceUrl}/api/v1/plugins/${encodeURIComponent(pluginId)}/icon`;
|
|
1980
|
+
}
|
|
1981
|
+
/**
|
|
1982
|
+
* Get plugin info from configured plugin and database state
|
|
1983
|
+
*/
|
|
1984
|
+
function buildPluginInfo(plugin, state, marketplaceUrl) {
|
|
1985
|
+
const status = state?.status ?? "active";
|
|
1986
|
+
const enabled = status === "active";
|
|
1987
|
+
const isMarketplace = (state?.source ?? "config") === "marketplace";
|
|
1988
|
+
return {
|
|
1989
|
+
id: plugin.id,
|
|
1990
|
+
name: state?.displayName || plugin.id,
|
|
1991
|
+
version: plugin.version,
|
|
1992
|
+
package: void 0,
|
|
1993
|
+
enabled,
|
|
1994
|
+
status,
|
|
1995
|
+
source: state?.source ?? "config",
|
|
1996
|
+
marketplaceVersion: state?.marketplaceVersion ?? void 0,
|
|
1997
|
+
registryPublisherDid: state?.registryPublisherDid ?? void 0,
|
|
1998
|
+
registrySlug: state?.registrySlug ?? void 0,
|
|
1999
|
+
capabilities: plugin.capabilities,
|
|
2000
|
+
hasAdminPages: (plugin.admin.pages?.length ?? 0) > 0,
|
|
2001
|
+
hasDashboardWidgets: (plugin.admin.widgets?.length ?? 0) > 0,
|
|
2002
|
+
hasHooks: Object.keys(plugin.hooks ?? {}).length > 0,
|
|
2003
|
+
installedAt: state?.installedAt?.toISOString(),
|
|
2004
|
+
activatedAt: state?.activatedAt?.toISOString() ?? void 0,
|
|
2005
|
+
deactivatedAt: state?.deactivatedAt?.toISOString() ?? void 0,
|
|
2006
|
+
description: state?.description ?? void 0,
|
|
2007
|
+
iconUrl: isMarketplace && marketplaceUrl ? marketplaceIconUrl(marketplaceUrl, plugin.id) : void 0
|
|
2008
|
+
};
|
|
2009
|
+
}
|
|
2010
|
+
/**
|
|
2011
|
+
* List all configured plugins with their state
|
|
2012
|
+
*/
|
|
2013
|
+
async function handlePluginList(db, configuredPlugins, marketplaceUrl) {
|
|
2014
|
+
try {
|
|
2015
|
+
const allStates = await new PluginStateRepository(db).getAll();
|
|
2016
|
+
const stateMap = new Map(allStates.map((s) => [s.pluginId, s]));
|
|
2017
|
+
const configuredIds = new Set(configuredPlugins.map((p) => p.id));
|
|
2018
|
+
const items = configuredPlugins.map((plugin) => {
|
|
2019
|
+
return buildPluginInfo(plugin, stateMap.get(plugin.id) ?? null, marketplaceUrl);
|
|
2020
|
+
});
|
|
2021
|
+
for (const state of allStates) {
|
|
2022
|
+
if (state.source !== "marketplace" && state.source !== "registry") continue;
|
|
2023
|
+
if (configuredIds.has(state.pluginId)) continue;
|
|
2024
|
+
items.push({
|
|
2025
|
+
id: state.pluginId,
|
|
2026
|
+
name: state.displayName || state.pluginId,
|
|
2027
|
+
version: state.marketplaceVersion ?? state.version,
|
|
2028
|
+
enabled: state.status === "active",
|
|
2029
|
+
status: state.status,
|
|
2030
|
+
source: state.source,
|
|
2031
|
+
marketplaceVersion: state.marketplaceVersion ?? void 0,
|
|
2032
|
+
registryPublisherDid: state.registryPublisherDid ?? void 0,
|
|
2033
|
+
registrySlug: state.registrySlug ?? void 0,
|
|
2034
|
+
capabilities: [],
|
|
2035
|
+
hasAdminPages: false,
|
|
2036
|
+
hasDashboardWidgets: false,
|
|
2037
|
+
hasHooks: false,
|
|
2038
|
+
installedAt: state.installedAt?.toISOString(),
|
|
2039
|
+
activatedAt: state.activatedAt?.toISOString() ?? void 0,
|
|
2040
|
+
deactivatedAt: state.deactivatedAt?.toISOString() ?? void 0,
|
|
2041
|
+
description: state.description ?? void 0,
|
|
2042
|
+
iconUrl: state.source === "marketplace" && marketplaceUrl ? marketplaceIconUrl(marketplaceUrl, state.pluginId) : void 0
|
|
2043
|
+
});
|
|
2044
|
+
}
|
|
2045
|
+
return {
|
|
2046
|
+
success: true,
|
|
2047
|
+
data: { items }
|
|
2048
|
+
};
|
|
2049
|
+
} catch {
|
|
2050
|
+
return {
|
|
2051
|
+
success: false,
|
|
2052
|
+
error: {
|
|
2053
|
+
code: "PLUGIN_LIST_ERROR",
|
|
2054
|
+
message: "Failed to list plugins"
|
|
2055
|
+
}
|
|
2056
|
+
};
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
/**
|
|
2060
|
+
* Get a single plugin's info
|
|
2061
|
+
*/
|
|
2062
|
+
async function handlePluginGet(db, configuredPlugins, pluginId, marketplaceUrl) {
|
|
2063
|
+
try {
|
|
2064
|
+
const plugin = configuredPlugins.find((p) => p.id === pluginId);
|
|
2065
|
+
if (!plugin) return {
|
|
2066
|
+
success: false,
|
|
2067
|
+
error: {
|
|
2068
|
+
code: "NOT_FOUND",
|
|
2069
|
+
message: `Plugin not found: ${pluginId}`
|
|
2070
|
+
}
|
|
2071
|
+
};
|
|
2072
|
+
return {
|
|
2073
|
+
success: true,
|
|
2074
|
+
data: { item: buildPluginInfo(plugin, await new PluginStateRepository(db).get(pluginId), marketplaceUrl) }
|
|
2075
|
+
};
|
|
2076
|
+
} catch {
|
|
2077
|
+
return {
|
|
2078
|
+
success: false,
|
|
2079
|
+
error: {
|
|
2080
|
+
code: "PLUGIN_GET_ERROR",
|
|
2081
|
+
message: "Failed to get plugin"
|
|
2082
|
+
}
|
|
2083
|
+
};
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
/**
|
|
2087
|
+
* Build a minimal `PluginInfo` for a plugin that exists only as a
|
|
2088
|
+
* `_plugin_state` row (marketplace or registry install), with no
|
|
2089
|
+
* matching `configuredPlugins` entry. Runtime-installed plugins don't
|
|
2090
|
+
* have ResolvedPlugin metadata until they're loaded into the sandbox,
|
|
2091
|
+
* so the enable/disable response surfaces the state-row view as a
|
|
2092
|
+
* stable shape the admin UI already understands.
|
|
2093
|
+
*/
|
|
2094
|
+
function buildStateOnlyPluginInfo(state) {
|
|
2095
|
+
return {
|
|
2096
|
+
id: state.pluginId,
|
|
2097
|
+
name: state.displayName || state.pluginId,
|
|
2098
|
+
version: state.marketplaceVersion ?? state.version,
|
|
2099
|
+
enabled: state.status === "active",
|
|
2100
|
+
status: state.status,
|
|
2101
|
+
source: state.source,
|
|
2102
|
+
marketplaceVersion: state.marketplaceVersion ?? void 0,
|
|
2103
|
+
registryPublisherDid: state.registryPublisherDid ?? void 0,
|
|
2104
|
+
registrySlug: state.registrySlug ?? void 0,
|
|
2105
|
+
capabilities: [],
|
|
2106
|
+
hasAdminPages: false,
|
|
2107
|
+
hasDashboardWidgets: false,
|
|
2108
|
+
hasHooks: false,
|
|
2109
|
+
installedAt: state.installedAt?.toISOString(),
|
|
2110
|
+
activatedAt: state.activatedAt?.toISOString() ?? void 0,
|
|
2111
|
+
deactivatedAt: state.deactivatedAt?.toISOString() ?? void 0,
|
|
2112
|
+
description: state.description ?? void 0
|
|
2113
|
+
};
|
|
2114
|
+
}
|
|
2115
|
+
/**
|
|
2116
|
+
* Enable a plugin
|
|
2117
|
+
*/
|
|
2118
|
+
async function handlePluginEnable(db, configuredPlugins, pluginId) {
|
|
2119
|
+
try {
|
|
2120
|
+
const stateRepo = new PluginStateRepository(db);
|
|
2121
|
+
const plugin = configuredPlugins.find((p) => p.id === pluginId);
|
|
2122
|
+
if (plugin) return {
|
|
2123
|
+
success: true,
|
|
2124
|
+
data: { item: buildPluginInfo(plugin, await stateRepo.enable(pluginId, plugin.version)) }
|
|
2125
|
+
};
|
|
2126
|
+
const existing = await stateRepo.get(pluginId);
|
|
2127
|
+
if (!existing || existing.source !== "marketplace" && existing.source !== "registry") return {
|
|
2128
|
+
success: false,
|
|
2129
|
+
error: {
|
|
2130
|
+
code: "NOT_FOUND",
|
|
2131
|
+
message: `Plugin not found: ${pluginId}`
|
|
2132
|
+
}
|
|
2133
|
+
};
|
|
2134
|
+
return {
|
|
2135
|
+
success: true,
|
|
2136
|
+
data: { item: buildStateOnlyPluginInfo(await stateRepo.enable(pluginId, existing.version)) }
|
|
2137
|
+
};
|
|
2138
|
+
} catch {
|
|
2139
|
+
return {
|
|
2140
|
+
success: false,
|
|
2141
|
+
error: {
|
|
2142
|
+
code: "PLUGIN_ENABLE_ERROR",
|
|
2143
|
+
message: "Failed to enable plugin"
|
|
2144
|
+
}
|
|
2145
|
+
};
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
/**
|
|
2149
|
+
* Disable a plugin
|
|
2150
|
+
*/
|
|
2151
|
+
async function handlePluginDisable(db, configuredPlugins, pluginId) {
|
|
2152
|
+
try {
|
|
2153
|
+
const stateRepo = new PluginStateRepository(db);
|
|
2154
|
+
const plugin = configuredPlugins.find((p) => p.id === pluginId);
|
|
2155
|
+
if (plugin) return {
|
|
2156
|
+
success: true,
|
|
2157
|
+
data: { item: buildPluginInfo(plugin, await stateRepo.disable(pluginId, plugin.version)) }
|
|
2158
|
+
};
|
|
2159
|
+
const existing = await stateRepo.get(pluginId);
|
|
2160
|
+
if (!existing || existing.source !== "marketplace" && existing.source !== "registry") return {
|
|
2161
|
+
success: false,
|
|
2162
|
+
error: {
|
|
2163
|
+
code: "NOT_FOUND",
|
|
2164
|
+
message: `Plugin not found: ${pluginId}`
|
|
2165
|
+
}
|
|
2166
|
+
};
|
|
2167
|
+
return {
|
|
2168
|
+
success: true,
|
|
2169
|
+
data: { item: buildStateOnlyPluginInfo(await stateRepo.disable(pluginId, existing.version)) }
|
|
2170
|
+
};
|
|
2171
|
+
} catch {
|
|
2172
|
+
return {
|
|
2173
|
+
success: false,
|
|
2174
|
+
error: {
|
|
2175
|
+
code: "PLUGIN_DISABLE_ERROR",
|
|
2176
|
+
message: "Failed to disable plugin"
|
|
2177
|
+
}
|
|
2178
|
+
};
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
//#endregion
|
|
2183
|
+
//#region src/plugins/marketplace.ts
|
|
2184
|
+
/**
|
|
2185
|
+
* MarketplaceClient — HTTP client for the EmDash Plugin Marketplace
|
|
2186
|
+
*
|
|
2187
|
+
* Used by the install/update/proxy endpoints in EmDash core to communicate
|
|
2188
|
+
* with the marketplace Worker. The marketplace is a distribution channel,
|
|
2189
|
+
* not a runtime dependency — bundles are copied to site-local R2 at install time.
|
|
2190
|
+
*/
|
|
2191
|
+
const TRAILING_SLASHES$1 = /\/+$/;
|
|
2192
|
+
const LEADING_DOT_SLASH = /^\.\//;
|
|
2193
|
+
var MarketplaceError = class extends Error {
|
|
2194
|
+
constructor(message, status, code) {
|
|
2195
|
+
super(message);
|
|
2196
|
+
this.status = status;
|
|
2197
|
+
this.code = code;
|
|
2198
|
+
this.name = "MarketplaceError";
|
|
2199
|
+
}
|
|
2200
|
+
};
|
|
2201
|
+
var MarketplaceUnavailableError = class extends MarketplaceError {
|
|
2202
|
+
constructor(cause) {
|
|
2203
|
+
super("Plugin marketplace is unavailable", void 0, "MARKETPLACE_UNAVAILABLE");
|
|
2204
|
+
if (cause) this.cause = cause;
|
|
2205
|
+
}
|
|
2206
|
+
};
|
|
2207
|
+
var MarketplaceClientImpl = class {
|
|
2208
|
+
baseUrl;
|
|
2209
|
+
siteOrigin;
|
|
2210
|
+
constructor(baseUrl, siteOrigin) {
|
|
2211
|
+
this.baseUrl = baseUrl.replace(TRAILING_SLASHES$1, "");
|
|
2212
|
+
this.siteOrigin = siteOrigin;
|
|
2213
|
+
}
|
|
2214
|
+
async search(query, opts) {
|
|
2215
|
+
const params = new URLSearchParams();
|
|
2216
|
+
if (query) params.set("q", query);
|
|
2217
|
+
if (opts?.category) params.set("category", opts.category);
|
|
2218
|
+
if (opts?.capability) params.set("capability", opts.capability);
|
|
2219
|
+
if (opts?.sort) params.set("sort", opts.sort);
|
|
2220
|
+
if (opts?.cursor) params.set("cursor", opts.cursor);
|
|
2221
|
+
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
2222
|
+
const qs = params.toString();
|
|
2223
|
+
const url = `${this.baseUrl}/api/v1/plugins${qs ? `?${qs}` : ""}`;
|
|
2224
|
+
return await this.fetchJson(url);
|
|
2225
|
+
}
|
|
2226
|
+
async getPlugin(id) {
|
|
2227
|
+
const url = `${this.baseUrl}/api/v1/plugins/${encodeURIComponent(id)}`;
|
|
2228
|
+
return this.fetchJson(url);
|
|
2229
|
+
}
|
|
2230
|
+
async getVersions(id) {
|
|
2231
|
+
const url = `${this.baseUrl}/api/v1/plugins/${encodeURIComponent(id)}/versions`;
|
|
2232
|
+
return (await this.fetchJson(url)).items;
|
|
2233
|
+
}
|
|
2234
|
+
async downloadBundle(id, version) {
|
|
2235
|
+
const bundleUrl = `${this.baseUrl}/api/v1/plugins/${encodeURIComponent(id)}/versions/${encodeURIComponent(version)}/bundle`;
|
|
2236
|
+
const marketplaceOrigin = new URL(this.baseUrl).origin;
|
|
2237
|
+
const MAX_REDIRECTS = 5;
|
|
2238
|
+
let response;
|
|
2239
|
+
try {
|
|
2240
|
+
let currentUrl = bundleUrl;
|
|
2241
|
+
response = await fetch(currentUrl, { redirect: "manual" });
|
|
2242
|
+
for (let i = 0; i < MAX_REDIRECTS; i++) {
|
|
2243
|
+
if (response.status < 300 || response.status >= 400) break;
|
|
2244
|
+
const location = response.headers.get("location");
|
|
2245
|
+
if (!location) break;
|
|
2246
|
+
const target = new URL(location, currentUrl);
|
|
2247
|
+
if (target.origin !== marketplaceOrigin) throw new MarketplaceError(`Bundle download redirected to untrusted host: ${target.origin}`, response.status, "BUNDLE_REDIRECT_UNTRUSTED");
|
|
2248
|
+
currentUrl = target.href;
|
|
2249
|
+
response = await fetch(currentUrl, { redirect: "manual" });
|
|
2250
|
+
}
|
|
2251
|
+
if (response.status >= 300 && response.status < 400) throw new MarketplaceError(`Bundle download exceeded maximum redirects (${MAX_REDIRECTS})`, response.status, "BUNDLE_TOO_MANY_REDIRECTS");
|
|
2252
|
+
} catch (err) {
|
|
2253
|
+
if (err instanceof MarketplaceError) throw err;
|
|
2254
|
+
throw new MarketplaceUnavailableError(err);
|
|
2255
|
+
}
|
|
2256
|
+
if (!response.ok) throw new MarketplaceError(`Failed to download bundle: ${response.status} ${response.statusText}`, response.status, "BUNDLE_DOWNLOAD_FAILED");
|
|
2257
|
+
const tarballBytes = new Uint8Array(await response.arrayBuffer());
|
|
2258
|
+
try {
|
|
2259
|
+
return await extractBundle(tarballBytes);
|
|
2260
|
+
} catch (err) {
|
|
2261
|
+
if (err instanceof MarketplaceError) throw err;
|
|
2262
|
+
throw new MarketplaceError("Failed to extract plugin bundle", void 0, "BUNDLE_EXTRACT_FAILED");
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
async reportInstall(id, version) {
|
|
2266
|
+
const siteHash = await generateSiteHash(this.siteOrigin);
|
|
2267
|
+
const url = `${this.baseUrl}/api/v1/plugins/${encodeURIComponent(id)}/installs`;
|
|
2268
|
+
try {
|
|
2269
|
+
await fetch(url, {
|
|
2270
|
+
method: "POST",
|
|
2271
|
+
headers: { "Content-Type": "application/json" },
|
|
2272
|
+
body: JSON.stringify({
|
|
2273
|
+
siteHash,
|
|
2274
|
+
version
|
|
2275
|
+
})
|
|
2276
|
+
});
|
|
2277
|
+
} catch {}
|
|
2278
|
+
}
|
|
2279
|
+
async searchThemes(query, opts) {
|
|
2280
|
+
const params = new URLSearchParams();
|
|
2281
|
+
if (query) params.set("q", query);
|
|
2282
|
+
if (opts?.keyword) params.set("keyword", opts.keyword);
|
|
2283
|
+
if (opts?.sort) params.set("sort", opts.sort);
|
|
2284
|
+
if (opts?.cursor) params.set("cursor", opts.cursor);
|
|
2285
|
+
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
2286
|
+
const qs = params.toString();
|
|
2287
|
+
const url = `${this.baseUrl}/api/v1/themes${qs ? `?${qs}` : ""}`;
|
|
2288
|
+
return this.fetchJson(url);
|
|
2289
|
+
}
|
|
2290
|
+
async getTheme(id) {
|
|
2291
|
+
const url = `${this.baseUrl}/api/v1/themes/${encodeURIComponent(id)}`;
|
|
2292
|
+
return this.fetchJson(url);
|
|
2293
|
+
}
|
|
2294
|
+
async fetchJson(url) {
|
|
2295
|
+
let response;
|
|
2296
|
+
try {
|
|
2297
|
+
response = await fetch(url, { headers: { Accept: "application/json" } });
|
|
2298
|
+
} catch (err) {
|
|
2299
|
+
throw new MarketplaceUnavailableError(err);
|
|
2300
|
+
}
|
|
2301
|
+
if (!response.ok) {
|
|
2302
|
+
let errorMessage = `Marketplace request failed: ${response.status}`;
|
|
2303
|
+
try {
|
|
2304
|
+
const body = await response.json();
|
|
2305
|
+
if (body.error) errorMessage = body.error;
|
|
2306
|
+
} catch {}
|
|
2307
|
+
throw new MarketplaceError(errorMessage, response.status);
|
|
2308
|
+
}
|
|
2309
|
+
return await response.json();
|
|
2310
|
+
}
|
|
2311
|
+
};
|
|
2312
|
+
/**
|
|
2313
|
+
* Extract manifest + code files from a tarball.
|
|
2314
|
+
*
|
|
2315
|
+
* The tarball is a gzipped tar archive containing:
|
|
2316
|
+
* - manifest.json
|
|
2317
|
+
* - backend.js
|
|
2318
|
+
* - admin.js (optional)
|
|
2319
|
+
*
|
|
2320
|
+
* We use a minimal tar parser since we only need to read a few small files.
|
|
2321
|
+
*/
|
|
2322
|
+
/**
|
|
2323
|
+
* Exported so the experimental registry install handler can reuse the
|
|
2324
|
+
* same parse / validate / hash primitive. Despite the file name, this
|
|
2325
|
+
* function predates the marketplace-vs-registry split and is generic
|
|
2326
|
+
* over plugin bundle tarballs regardless of distribution channel.
|
|
2327
|
+
*/
|
|
2328
|
+
const MAX_DECOMPRESSED_BUNDLE_BYTES = 256 * 1024;
|
|
2329
|
+
const MAX_BUNDLE_TAR_ENTRIES = 32;
|
|
2330
|
+
async function extractBundle(tarballBytes) {
|
|
2331
|
+
const reader = new ReadableStream({ start(controller) {
|
|
2332
|
+
controller.enqueue(tarballBytes);
|
|
2333
|
+
controller.close();
|
|
2334
|
+
} }).pipeThrough(createGzipDecoder()).getReader();
|
|
2335
|
+
const chunks = [];
|
|
2336
|
+
let total = 0;
|
|
2337
|
+
while (true) {
|
|
2338
|
+
const { done, value } = await reader.read();
|
|
2339
|
+
if (done) break;
|
|
2340
|
+
if (!value) continue;
|
|
2341
|
+
total += value.byteLength;
|
|
2342
|
+
if (total > MAX_DECOMPRESSED_BUNDLE_BYTES) {
|
|
2343
|
+
try {
|
|
2344
|
+
await reader.cancel();
|
|
2345
|
+
} catch {}
|
|
2346
|
+
throw new MarketplaceError(`Bundle decompressed size exceeds limit (${MAX_DECOMPRESSED_BUNDLE_BYTES} bytes)`, void 0, "INVALID_BUNDLE");
|
|
2347
|
+
}
|
|
2348
|
+
chunks.push(value);
|
|
2349
|
+
}
|
|
2350
|
+
const decompressedBytes = new Uint8Array(total);
|
|
2351
|
+
{
|
|
2352
|
+
let offset = 0;
|
|
2353
|
+
for (const chunk of chunks) {
|
|
2354
|
+
decompressedBytes.set(chunk, offset);
|
|
2355
|
+
offset += chunk.byteLength;
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
const entries = await unpackTar(new ReadableStream({ start(controller) {
|
|
2359
|
+
controller.enqueue(decompressedBytes);
|
|
2360
|
+
controller.close();
|
|
2361
|
+
} }));
|
|
2362
|
+
if (entries.length > MAX_BUNDLE_TAR_ENTRIES) throw new MarketplaceError(`Bundle has too many tar entries (${entries.length} > ${MAX_BUNDLE_TAR_ENTRIES})`, void 0, "INVALID_BUNDLE");
|
|
2363
|
+
const decoder = new TextDecoder();
|
|
2364
|
+
const files = /* @__PURE__ */ new Map();
|
|
2365
|
+
for (const entry of entries) if (entry.data && entry.header.type === "file") {
|
|
2366
|
+
const name = entry.header.name.replace(LEADING_DOT_SLASH, "");
|
|
2367
|
+
files.set(name, decoder.decode(entry.data));
|
|
2368
|
+
}
|
|
2369
|
+
const manifestJson = files.get("manifest.json");
|
|
2370
|
+
const backendCode = files.get("backend.js");
|
|
2371
|
+
if (!manifestJson) throw new MarketplaceError("Invalid bundle: missing manifest.json", void 0, "INVALID_BUNDLE");
|
|
2372
|
+
if (!backendCode) throw new MarketplaceError("Invalid bundle: missing backend.js", void 0, "INVALID_BUNDLE");
|
|
2373
|
+
let manifest;
|
|
2374
|
+
try {
|
|
2375
|
+
const parsed = JSON.parse(manifestJson);
|
|
2376
|
+
const result = pluginManifestSchema.safeParse(parsed);
|
|
2377
|
+
if (!result.success) throw new MarketplaceError("Invalid bundle: manifest.json failed validation", void 0, "INVALID_BUNDLE");
|
|
2378
|
+
manifest = result.data;
|
|
2379
|
+
} catch (err) {
|
|
2380
|
+
if (err instanceof MarketplaceError) throw err;
|
|
2381
|
+
throw new MarketplaceError("Invalid bundle: malformed manifest.json", void 0, "INVALID_BUNDLE");
|
|
2382
|
+
}
|
|
2383
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", tarballBytes);
|
|
2384
|
+
const hashArray = new Uint8Array(hashBuffer);
|
|
2385
|
+
const checksum = Array.from(hashArray, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
2386
|
+
return {
|
|
2387
|
+
manifest,
|
|
2388
|
+
backendCode,
|
|
2389
|
+
adminCode: files.get("admin.js"),
|
|
2390
|
+
checksum
|
|
2391
|
+
};
|
|
2392
|
+
}
|
|
2393
|
+
/**
|
|
2394
|
+
* Generate a stable non-identifying site hash from the site origin.
|
|
2395
|
+
* The same origin always produces the same hash, so the marketplace
|
|
2396
|
+
* installs table deduplicates correctly per (plugin_id, site_hash).
|
|
2397
|
+
*/
|
|
2398
|
+
async function generateSiteHash(siteOrigin) {
|
|
2399
|
+
const seed = siteOrigin ? `emdash-site:${siteOrigin}` : `emdash-anonymous`;
|
|
2400
|
+
try {
|
|
2401
|
+
const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(seed));
|
|
2402
|
+
const arr = new Uint8Array(hash);
|
|
2403
|
+
return Array.from(arr.slice(0, 8), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
2404
|
+
} catch {
|
|
2405
|
+
let h = 2166136261;
|
|
2406
|
+
for (let i = 0; i < seed.length; i++) {
|
|
2407
|
+
h ^= seed.charCodeAt(i);
|
|
2408
|
+
h = Math.imul(h, 16777619);
|
|
2409
|
+
}
|
|
2410
|
+
const h2 = h ^ h >>> 16;
|
|
2411
|
+
return (h >>> 0).toString(16).padStart(8, "0") + (h2 >>> 0).toString(16).padStart(8, "0");
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
/**
|
|
2415
|
+
* Create a MarketplaceClient for the given marketplace URL.
|
|
2416
|
+
*
|
|
2417
|
+
* @param baseUrl - The marketplace API base URL (e.g. "https://marketplace.emdashcms.com")
|
|
2418
|
+
* @param siteOrigin - The origin of the EmDash site (e.g. "https://myblog.example.com").
|
|
2419
|
+
* Used to generate a stable, non-identifying site hash for install deduplication.
|
|
2420
|
+
*/
|
|
2421
|
+
function createMarketplaceClient(baseUrl, siteOrigin) {
|
|
2422
|
+
return new MarketplaceClientImpl(baseUrl, siteOrigin);
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
//#endregion
|
|
2426
|
+
//#region src/api/handlers/marketplace.ts
|
|
2427
|
+
/** Semver-like pattern: digits, dots, hyphens, plus signs (e.g. 1.0.0, 1.0.0-beta.1) */
|
|
2428
|
+
const VERSION_PATTERN = /^[a-z0-9][a-z0-9._+-]*$/i;
|
|
2429
|
+
function validateVersion(version) {
|
|
2430
|
+
if (version.includes("..")) throw new Error("Invalid version format");
|
|
2431
|
+
if (!VERSION_PATTERN.test(version)) throw new Error("Invalid version format");
|
|
2432
|
+
}
|
|
2433
|
+
function getClient(marketplaceUrl, siteOrigin) {
|
|
2434
|
+
if (!marketplaceUrl) return null;
|
|
2435
|
+
return createMarketplaceClient(marketplaceUrl, siteOrigin);
|
|
2436
|
+
}
|
|
2437
|
+
function diffCapabilities(oldCaps, newCaps) {
|
|
2438
|
+
const oldNorm = normalizeCapabilities(oldCaps);
|
|
2439
|
+
const newNorm = normalizeCapabilities(newCaps);
|
|
2440
|
+
const oldSet = new Set(oldNorm);
|
|
2441
|
+
const newSet = new Set(newNorm);
|
|
2442
|
+
return {
|
|
2443
|
+
added: newNorm.filter((c) => !oldSet.has(c)),
|
|
2444
|
+
removed: oldNorm.filter((c) => !newSet.has(c))
|
|
2445
|
+
};
|
|
2446
|
+
}
|
|
2447
|
+
/**
|
|
2448
|
+
* Diff route visibility between two manifests.
|
|
2449
|
+
* Returns routes that changed from private to public (newly exposed).
|
|
2450
|
+
*/
|
|
2451
|
+
function diffRouteVisibility(oldManifest, newManifest) {
|
|
2452
|
+
const oldPublicRoutes = /* @__PURE__ */ new Set();
|
|
2453
|
+
if (oldManifest) for (const entry of oldManifest.routes) {
|
|
2454
|
+
const normalized = normalizeManifestRoute(entry);
|
|
2455
|
+
if (normalized.public === true) oldPublicRoutes.add(normalized.name);
|
|
2456
|
+
}
|
|
2457
|
+
const newlyPublic = [];
|
|
2458
|
+
for (const entry of newManifest.routes) {
|
|
2459
|
+
const normalized = normalizeManifestRoute(entry);
|
|
2460
|
+
if (normalized.public === true && !oldPublicRoutes.has(normalized.name)) newlyPublic.push(normalized.name);
|
|
2461
|
+
}
|
|
2462
|
+
return { newlyPublic };
|
|
2463
|
+
}
|
|
2464
|
+
async function resolveVersionMetadata(client, pluginId, pluginDetail, version) {
|
|
2465
|
+
if (pluginDetail.latestVersion?.version === version) return {
|
|
2466
|
+
version: pluginDetail.latestVersion.version,
|
|
2467
|
+
minEmDashVersion: pluginDetail.latestVersion.minEmDashVersion,
|
|
2468
|
+
bundleSize: pluginDetail.latestVersion.bundleSize,
|
|
2469
|
+
checksum: pluginDetail.latestVersion.checksum,
|
|
2470
|
+
changelog: pluginDetail.latestVersion.changelog,
|
|
2471
|
+
capabilities: pluginDetail.latestVersion.capabilities,
|
|
2472
|
+
status: pluginDetail.latestVersion.status,
|
|
2473
|
+
auditVerdict: pluginDetail.latestVersion.audit?.verdict ?? null,
|
|
2474
|
+
imageAuditVerdict: pluginDetail.latestVersion.imageAudit?.verdict ?? null,
|
|
2475
|
+
publishedAt: pluginDetail.latestVersion.publishedAt
|
|
2476
|
+
};
|
|
2477
|
+
return (await client.getVersions(pluginId)).find((v) => v.version === version) ?? null;
|
|
2478
|
+
}
|
|
2479
|
+
function validateBundleIdentity(bundle, pluginId, version) {
|
|
2480
|
+
if (bundle.manifest.id !== pluginId) return {
|
|
2481
|
+
success: false,
|
|
2482
|
+
error: {
|
|
2483
|
+
code: "MANIFEST_MISMATCH",
|
|
2484
|
+
message: `Bundle manifest ID (${bundle.manifest.id}) does not match requested plugin (${pluginId})`
|
|
2485
|
+
}
|
|
2486
|
+
};
|
|
2487
|
+
if (bundle.manifest.version !== version) return {
|
|
2488
|
+
success: false,
|
|
2489
|
+
error: {
|
|
2490
|
+
code: "MANIFEST_VERSION_MISMATCH",
|
|
2491
|
+
message: `Bundle manifest version (${bundle.manifest.version}) does not match requested version (${version})`
|
|
2492
|
+
}
|
|
2493
|
+
};
|
|
2494
|
+
return null;
|
|
2495
|
+
}
|
|
2496
|
+
function bundlePrefix(source, pluginId, version) {
|
|
2497
|
+
return `${source}/${pluginId}/${version}`;
|
|
2498
|
+
}
|
|
2499
|
+
async function storeBundleInR2(storage, pluginId, version, bundle, source = "marketplace") {
|
|
2500
|
+
validatePluginIdentifier(pluginId, "plugin ID");
|
|
2501
|
+
validateVersion(version);
|
|
2502
|
+
const prefix = bundlePrefix(source, pluginId, version);
|
|
2503
|
+
await storage.upload({
|
|
2504
|
+
key: `${prefix}/manifest.json`,
|
|
2505
|
+
body: new TextEncoder().encode(JSON.stringify(bundle.manifest)),
|
|
2506
|
+
contentType: "application/json"
|
|
2507
|
+
});
|
|
2508
|
+
await storage.upload({
|
|
2509
|
+
key: `${prefix}/backend.js`,
|
|
2510
|
+
body: new TextEncoder().encode(bundle.backendCode),
|
|
2511
|
+
contentType: "application/javascript"
|
|
2512
|
+
});
|
|
2513
|
+
if (bundle.adminCode) await storage.upload({
|
|
2514
|
+
key: `${prefix}/admin.js`,
|
|
2515
|
+
body: new TextEncoder().encode(bundle.adminCode),
|
|
2516
|
+
contentType: "application/javascript"
|
|
2517
|
+
});
|
|
2518
|
+
}
|
|
2519
|
+
/** Read a ReadableStream to string */
|
|
2520
|
+
async function streamToText(stream) {
|
|
2521
|
+
return new Response(stream).text();
|
|
2522
|
+
}
|
|
2523
|
+
/**
|
|
2524
|
+
* Load a plugin bundle from site-local R2 storage.
|
|
2525
|
+
*
|
|
2526
|
+
* `source` selects the R2 key prefix: marketplace plugins are stored
|
|
2527
|
+
* under `marketplace/<id>/<version>/`, registry plugins under
|
|
2528
|
+
* `registry/<id>/<version>/`. Defaults to `"marketplace"` for
|
|
2529
|
+
* backwards compatibility with pre-registry call sites.
|
|
2530
|
+
*/
|
|
2531
|
+
async function loadBundleFromR2(storage, pluginId, version, source = "marketplace") {
|
|
2532
|
+
validatePluginIdentifier(pluginId, "plugin ID");
|
|
2533
|
+
validateVersion(version);
|
|
2534
|
+
const prefix = bundlePrefix(source, pluginId, version);
|
|
2535
|
+
try {
|
|
2536
|
+
const manifestResult = await storage.download(`${prefix}/manifest.json`);
|
|
2537
|
+
const backendResult = await storage.download(`${prefix}/backend.js`);
|
|
2538
|
+
const manifestText = await streamToText(manifestResult.body);
|
|
2539
|
+
const backendCode = await streamToText(backendResult.body);
|
|
2540
|
+
const parsed = JSON.parse(manifestText);
|
|
2541
|
+
const result = pluginManifestSchema.safeParse(parsed);
|
|
2542
|
+
if (!result.success) return null;
|
|
2543
|
+
const manifest = result.data;
|
|
2544
|
+
let adminCode;
|
|
2545
|
+
try {
|
|
2546
|
+
adminCode = await streamToText((await storage.download(`${prefix}/admin.js`)).body);
|
|
2547
|
+
} catch {}
|
|
2548
|
+
return {
|
|
2549
|
+
manifest,
|
|
2550
|
+
backendCode,
|
|
2551
|
+
adminCode
|
|
2552
|
+
};
|
|
2553
|
+
} catch {
|
|
2554
|
+
return null;
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
/** Delete a plugin bundle from site-local R2 storage */
|
|
2558
|
+
async function deleteBundleFromR2(storage, pluginId, version, source = "marketplace") {
|
|
2559
|
+
validatePluginIdentifier(pluginId, "plugin ID");
|
|
2560
|
+
validateVersion(version);
|
|
2561
|
+
const prefix = bundlePrefix(source, pluginId, version);
|
|
2562
|
+
for (const file of [
|
|
2563
|
+
"manifest.json",
|
|
2564
|
+
"backend.js",
|
|
2565
|
+
"admin.js"
|
|
2566
|
+
]) try {
|
|
2567
|
+
await storage.delete(`${prefix}/${file}`);
|
|
2568
|
+
} catch {}
|
|
2569
|
+
}
|
|
2570
|
+
async function handleMarketplaceInstall(db, storage, sandboxRunner, marketplaceUrl, pluginId, opts) {
|
|
2571
|
+
const client = getClient(marketplaceUrl, opts?.siteOrigin);
|
|
2572
|
+
if (!client) return {
|
|
2573
|
+
success: false,
|
|
2574
|
+
error: {
|
|
2575
|
+
code: "MARKETPLACE_NOT_CONFIGURED",
|
|
2576
|
+
message: "Marketplace is not configured"
|
|
2577
|
+
}
|
|
2578
|
+
};
|
|
2579
|
+
if (!storage) return {
|
|
2580
|
+
success: false,
|
|
2581
|
+
error: {
|
|
2582
|
+
code: "STORAGE_NOT_CONFIGURED",
|
|
2583
|
+
message: "Storage is required for marketplace plugin installation"
|
|
2584
|
+
}
|
|
2585
|
+
};
|
|
2586
|
+
if (!sandboxRunner || !sandboxRunner.isAvailable()) return {
|
|
2587
|
+
success: false,
|
|
2588
|
+
error: {
|
|
2589
|
+
code: "SANDBOX_NOT_AVAILABLE",
|
|
2590
|
+
message: "Sandbox runner is required for marketplace plugins"
|
|
2591
|
+
}
|
|
2592
|
+
};
|
|
2593
|
+
try {
|
|
2594
|
+
const stateRepo = new PluginStateRepository(db);
|
|
2595
|
+
const existing = await stateRepo.get(pluginId);
|
|
2596
|
+
if (existing && existing.source === "marketplace") return {
|
|
2597
|
+
success: false,
|
|
2598
|
+
error: {
|
|
2599
|
+
code: "ALREADY_INSTALLED",
|
|
2600
|
+
message: `Plugin ${pluginId} is already installed`
|
|
2601
|
+
}
|
|
2602
|
+
};
|
|
2603
|
+
if (opts?.configuredPluginIds?.has(pluginId)) return {
|
|
2604
|
+
success: false,
|
|
2605
|
+
error: {
|
|
2606
|
+
code: "PLUGIN_ID_CONFLICT",
|
|
2607
|
+
message: `Cannot install marketplace plugin "${pluginId}" — a configured plugin with the same ID already exists`
|
|
2608
|
+
}
|
|
2609
|
+
};
|
|
2610
|
+
const pluginDetail = await client.getPlugin(pluginId);
|
|
2611
|
+
const version = opts?.version ?? pluginDetail.latestVersion?.version;
|
|
2612
|
+
if (!version) return {
|
|
2613
|
+
success: false,
|
|
2614
|
+
error: {
|
|
2615
|
+
code: "NO_VERSION",
|
|
2616
|
+
message: `No published versions found for plugin ${pluginId}`
|
|
2617
|
+
}
|
|
2618
|
+
};
|
|
2619
|
+
const versionMetadata = await resolveVersionMetadata(client, pluginId, pluginDetail, version);
|
|
2620
|
+
if (!versionMetadata) return {
|
|
2621
|
+
success: false,
|
|
2622
|
+
error: {
|
|
2623
|
+
code: "NO_VERSION",
|
|
2624
|
+
message: `Version ${version} was not found for plugin ${pluginId}`
|
|
2625
|
+
}
|
|
2626
|
+
};
|
|
2627
|
+
if (versionMetadata.auditVerdict === "fail" || versionMetadata.auditVerdict === "warn") return {
|
|
2628
|
+
success: false,
|
|
2629
|
+
error: {
|
|
2630
|
+
code: "AUDIT_FAILED",
|
|
2631
|
+
message: versionMetadata.auditVerdict === "fail" ? "Plugin failed security audit and cannot be installed" : "Plugin audit was inconclusive and cannot be installed until reviewed"
|
|
2632
|
+
}
|
|
2633
|
+
};
|
|
2634
|
+
const bundle = await client.downloadBundle(pluginId, version);
|
|
2635
|
+
if (versionMetadata.checksum && bundle.checksum !== versionMetadata.checksum) return {
|
|
2636
|
+
success: false,
|
|
2637
|
+
error: {
|
|
2638
|
+
code: "CHECKSUM_MISMATCH",
|
|
2639
|
+
message: "Bundle checksum does not match marketplace record. Download may be corrupted."
|
|
2640
|
+
}
|
|
2641
|
+
};
|
|
2642
|
+
const bundleIdentityError = validateBundleIdentity(bundle, pluginId, version);
|
|
2643
|
+
if (bundleIdentityError) return bundleIdentityError;
|
|
2644
|
+
await storeBundleInR2(storage, pluginId, version, bundle);
|
|
2645
|
+
await stateRepo.upsert(pluginId, version, "active", {
|
|
2646
|
+
source: "marketplace",
|
|
2647
|
+
marketplaceVersion: version,
|
|
2648
|
+
displayName: pluginDetail.name,
|
|
2649
|
+
description: pluginDetail.description ?? void 0
|
|
2650
|
+
});
|
|
2651
|
+
client.reportInstall(pluginId, version).catch(() => {});
|
|
2652
|
+
return {
|
|
2653
|
+
success: true,
|
|
2654
|
+
data: {
|
|
2655
|
+
pluginId,
|
|
2656
|
+
version,
|
|
2657
|
+
capabilities: bundle.manifest.capabilities
|
|
2658
|
+
}
|
|
2659
|
+
};
|
|
2660
|
+
} catch (err) {
|
|
2661
|
+
if (err instanceof MarketplaceUnavailableError) return {
|
|
2662
|
+
success: false,
|
|
2663
|
+
error: {
|
|
2664
|
+
code: "MARKETPLACE_UNAVAILABLE",
|
|
2665
|
+
message: "Plugin marketplace is currently unavailable"
|
|
2666
|
+
}
|
|
2667
|
+
};
|
|
2668
|
+
if (err instanceof MarketplaceError) return {
|
|
2669
|
+
success: false,
|
|
2670
|
+
error: {
|
|
2671
|
+
code: err.code ?? "MARKETPLACE_ERROR",
|
|
2672
|
+
message: err.message
|
|
2673
|
+
}
|
|
2674
|
+
};
|
|
2675
|
+
if (err instanceof EmDashStorageError) return {
|
|
2676
|
+
success: false,
|
|
2677
|
+
error: {
|
|
2678
|
+
code: err.code ?? "STORAGE_ERROR",
|
|
2679
|
+
message: "Storage error while installing plugin"
|
|
2680
|
+
}
|
|
2681
|
+
};
|
|
2682
|
+
if (err && typeof err === "object" && "code" in err) {
|
|
2683
|
+
const code = err.code;
|
|
2684
|
+
if (typeof code === "string" && code.trim()) return {
|
|
2685
|
+
success: false,
|
|
2686
|
+
error: {
|
|
2687
|
+
code,
|
|
2688
|
+
message: "Failed to install plugin from marketplace"
|
|
2689
|
+
}
|
|
2690
|
+
};
|
|
2691
|
+
}
|
|
2692
|
+
console.error("Failed to install marketplace plugin:", err);
|
|
2693
|
+
return {
|
|
2694
|
+
success: false,
|
|
2695
|
+
error: {
|
|
2696
|
+
code: "INSTALL_FAILED",
|
|
2697
|
+
message: "Failed to install plugin from marketplace"
|
|
2698
|
+
}
|
|
2699
|
+
};
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
async function handleMarketplaceUpdate(db, storage, sandboxRunner, marketplaceUrl, pluginId, opts) {
|
|
2703
|
+
const client = getClient(marketplaceUrl);
|
|
2704
|
+
if (!client) return {
|
|
2705
|
+
success: false,
|
|
2706
|
+
error: {
|
|
2707
|
+
code: "MARKETPLACE_NOT_CONFIGURED",
|
|
2708
|
+
message: "Marketplace is not configured"
|
|
2709
|
+
}
|
|
2710
|
+
};
|
|
2711
|
+
if (!storage) return {
|
|
2712
|
+
success: false,
|
|
2713
|
+
error: {
|
|
2714
|
+
code: "STORAGE_NOT_CONFIGURED",
|
|
2715
|
+
message: "Storage is required"
|
|
2716
|
+
}
|
|
2717
|
+
};
|
|
2718
|
+
if (!sandboxRunner || !sandboxRunner.isAvailable()) return {
|
|
2719
|
+
success: false,
|
|
2720
|
+
error: {
|
|
2721
|
+
code: "SANDBOX_NOT_AVAILABLE",
|
|
2722
|
+
message: "Sandbox runner is required"
|
|
2723
|
+
}
|
|
2724
|
+
};
|
|
2725
|
+
try {
|
|
2726
|
+
const stateRepo = new PluginStateRepository(db);
|
|
2727
|
+
const existing = await stateRepo.get(pluginId);
|
|
2728
|
+
if (!existing || existing.source !== "marketplace") return {
|
|
2729
|
+
success: false,
|
|
2730
|
+
error: {
|
|
2731
|
+
code: "NOT_FOUND",
|
|
2732
|
+
message: `No marketplace plugin found: ${pluginId}`
|
|
2733
|
+
}
|
|
2734
|
+
};
|
|
2735
|
+
const oldVersion = existing.marketplaceVersion ?? existing.version;
|
|
2736
|
+
const pluginDetail = await client.getPlugin(pluginId);
|
|
2737
|
+
const newVersion = opts?.version ?? pluginDetail.latestVersion?.version;
|
|
2738
|
+
if (!newVersion) return {
|
|
2739
|
+
success: false,
|
|
2740
|
+
error: {
|
|
2741
|
+
code: "NO_VERSION",
|
|
2742
|
+
message: "No newer version available"
|
|
2743
|
+
}
|
|
2744
|
+
};
|
|
2745
|
+
if (newVersion === oldVersion) return {
|
|
2746
|
+
success: false,
|
|
2747
|
+
error: {
|
|
2748
|
+
code: "ALREADY_UP_TO_DATE",
|
|
2749
|
+
message: "Plugin is already up to date"
|
|
2750
|
+
}
|
|
2751
|
+
};
|
|
2752
|
+
const versionMetadata = await resolveVersionMetadata(client, pluginId, pluginDetail, newVersion);
|
|
2753
|
+
if (!versionMetadata) return {
|
|
2754
|
+
success: false,
|
|
2755
|
+
error: {
|
|
2756
|
+
code: "NO_VERSION",
|
|
2757
|
+
message: `Version ${newVersion} was not found for plugin ${pluginId}`
|
|
2758
|
+
}
|
|
2759
|
+
};
|
|
2760
|
+
const bundle = await client.downloadBundle(pluginId, newVersion);
|
|
2761
|
+
if (versionMetadata.checksum && bundle.checksum !== versionMetadata.checksum) return {
|
|
2762
|
+
success: false,
|
|
2763
|
+
error: {
|
|
2764
|
+
code: "CHECKSUM_MISMATCH",
|
|
2765
|
+
message: "Bundle checksum does not match marketplace record. Download may be corrupted."
|
|
2766
|
+
}
|
|
2767
|
+
};
|
|
2768
|
+
const bundleIdentityError = validateBundleIdentity(bundle, pluginId, newVersion);
|
|
2769
|
+
if (bundleIdentityError) return bundleIdentityError;
|
|
2770
|
+
const oldBundle = await loadBundleFromR2(storage, pluginId, oldVersion);
|
|
2771
|
+
const capabilityChanges = diffCapabilities(oldBundle?.manifest.capabilities ?? [], bundle.manifest.capabilities);
|
|
2772
|
+
if (capabilityChanges.added.length > 0 && !opts?.confirmCapabilityChanges) return {
|
|
2773
|
+
success: false,
|
|
2774
|
+
error: {
|
|
2775
|
+
code: "CAPABILITY_ESCALATION",
|
|
2776
|
+
message: "Plugin update requires new capabilities",
|
|
2777
|
+
details: { capabilityChanges }
|
|
2778
|
+
}
|
|
2779
|
+
};
|
|
2780
|
+
const routeVisibilityChanges = diffRouteVisibility(oldBundle?.manifest, bundle.manifest);
|
|
2781
|
+
const hasNewPublicRoutes = routeVisibilityChanges.newlyPublic.length > 0;
|
|
2782
|
+
if (hasNewPublicRoutes && !opts?.confirmRouteVisibilityChanges) return {
|
|
2783
|
+
success: false,
|
|
2784
|
+
error: {
|
|
2785
|
+
code: "ROUTE_VISIBILITY_ESCALATION",
|
|
2786
|
+
message: "Plugin update exposes new public (unauthenticated) routes",
|
|
2787
|
+
details: {
|
|
2788
|
+
routeVisibilityChanges,
|
|
2789
|
+
capabilityChanges
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
};
|
|
2793
|
+
await storeBundleInR2(storage, pluginId, newVersion, bundle);
|
|
2794
|
+
await stateRepo.upsert(pluginId, newVersion, "active", {
|
|
2795
|
+
source: "marketplace",
|
|
2796
|
+
marketplaceVersion: newVersion,
|
|
2797
|
+
displayName: pluginDetail.name,
|
|
2798
|
+
description: pluginDetail.description ?? void 0
|
|
2799
|
+
});
|
|
2800
|
+
deleteBundleFromR2(storage, pluginId, oldVersion).catch(() => {});
|
|
2801
|
+
return {
|
|
2802
|
+
success: true,
|
|
2803
|
+
data: {
|
|
2804
|
+
pluginId,
|
|
2805
|
+
oldVersion,
|
|
2806
|
+
newVersion,
|
|
2807
|
+
capabilityChanges,
|
|
2808
|
+
routeVisibilityChanges: hasNewPublicRoutes ? routeVisibilityChanges : void 0
|
|
2809
|
+
}
|
|
2810
|
+
};
|
|
2811
|
+
} catch (err) {
|
|
2812
|
+
if (err instanceof MarketplaceUnavailableError) return {
|
|
2813
|
+
success: false,
|
|
2814
|
+
error: {
|
|
2815
|
+
code: "MARKETPLACE_UNAVAILABLE",
|
|
2816
|
+
message: "Marketplace is unavailable"
|
|
2817
|
+
}
|
|
2818
|
+
};
|
|
2819
|
+
if (err instanceof MarketplaceError) return {
|
|
2820
|
+
success: false,
|
|
2821
|
+
error: {
|
|
2822
|
+
code: err.code ?? "MARKETPLACE_ERROR",
|
|
2823
|
+
message: err.message
|
|
2824
|
+
}
|
|
2825
|
+
};
|
|
2826
|
+
console.error("Failed to update marketplace plugin:", err);
|
|
2827
|
+
return {
|
|
2828
|
+
success: false,
|
|
2829
|
+
error: {
|
|
2830
|
+
code: "UPDATE_FAILED",
|
|
2831
|
+
message: "Failed to update plugin"
|
|
2832
|
+
}
|
|
2833
|
+
};
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
async function handleMarketplaceUninstall(db, storage, pluginId, opts) {
|
|
2837
|
+
try {
|
|
2838
|
+
const stateRepo = new PluginStateRepository(db);
|
|
2839
|
+
const existing = await stateRepo.get(pluginId);
|
|
2840
|
+
if (!existing || existing.source !== "marketplace") return {
|
|
2841
|
+
success: false,
|
|
2842
|
+
error: {
|
|
2843
|
+
code: "NOT_FOUND",
|
|
2844
|
+
message: `No marketplace plugin found: ${pluginId}`
|
|
2845
|
+
}
|
|
2846
|
+
};
|
|
2847
|
+
const version = existing.marketplaceVersion ?? existing.version;
|
|
2848
|
+
if (storage) await deleteBundleFromR2(storage, pluginId, version);
|
|
2849
|
+
let dataDeleted = false;
|
|
2850
|
+
if (opts?.deleteData) try {
|
|
2851
|
+
await db.deleteFrom("_plugin_storage").where("plugin_id", "=", pluginId).execute();
|
|
2852
|
+
dataDeleted = true;
|
|
2853
|
+
} catch {}
|
|
2854
|
+
await stateRepo.delete(pluginId);
|
|
2855
|
+
return {
|
|
2856
|
+
success: true,
|
|
2857
|
+
data: {
|
|
2858
|
+
pluginId,
|
|
2859
|
+
dataDeleted
|
|
2860
|
+
}
|
|
2861
|
+
};
|
|
2862
|
+
} catch (err) {
|
|
2863
|
+
console.error("Failed to uninstall marketplace plugin:", err);
|
|
2864
|
+
return {
|
|
2865
|
+
success: false,
|
|
2866
|
+
error: {
|
|
2867
|
+
code: "UNINSTALL_FAILED",
|
|
2868
|
+
message: "Failed to uninstall plugin"
|
|
2869
|
+
}
|
|
2870
|
+
};
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
async function handleMarketplaceUpdateCheck(db, marketplaceUrl) {
|
|
2874
|
+
const client = getClient(marketplaceUrl);
|
|
2875
|
+
if (!client) return {
|
|
2876
|
+
success: false,
|
|
2877
|
+
error: {
|
|
2878
|
+
code: "MARKETPLACE_NOT_CONFIGURED",
|
|
2879
|
+
message: "Marketplace is not configured"
|
|
2880
|
+
}
|
|
2881
|
+
};
|
|
2882
|
+
try {
|
|
2883
|
+
const marketplacePlugins = await new PluginStateRepository(db).getMarketplacePlugins();
|
|
2884
|
+
const items = [];
|
|
2885
|
+
for (const plugin of marketplacePlugins) try {
|
|
2886
|
+
const detail = await client.getPlugin(plugin.pluginId);
|
|
2887
|
+
const latest = detail.latestVersion?.version;
|
|
2888
|
+
const installed = plugin.marketplaceVersion ?? plugin.version;
|
|
2889
|
+
if (!latest) continue;
|
|
2890
|
+
const hasUpdate = latest !== installed;
|
|
2891
|
+
let capabilityChanges;
|
|
2892
|
+
let hasCapabilityChanges = false;
|
|
2893
|
+
if (hasUpdate && detail.latestVersion) {
|
|
2894
|
+
capabilityChanges = diffCapabilities(detail.capabilities ?? [], detail.latestVersion.capabilities ?? []);
|
|
2895
|
+
hasCapabilityChanges = capabilityChanges.added.length > 0 || capabilityChanges.removed.length > 0;
|
|
2896
|
+
}
|
|
2897
|
+
items.push({
|
|
2898
|
+
pluginId: plugin.pluginId,
|
|
2899
|
+
installed,
|
|
2900
|
+
latest: latest ?? installed,
|
|
2901
|
+
hasUpdate,
|
|
2902
|
+
hasCapabilityChanges,
|
|
2903
|
+
capabilityChanges: hasCapabilityChanges ? capabilityChanges : void 0,
|
|
2904
|
+
hasRouteVisibilityChanges: false
|
|
2905
|
+
});
|
|
2906
|
+
} catch (err) {
|
|
2907
|
+
console.warn(`Failed to check updates for ${plugin.pluginId}:`, err);
|
|
2908
|
+
}
|
|
2909
|
+
return {
|
|
2910
|
+
success: true,
|
|
2911
|
+
data: { items }
|
|
2912
|
+
};
|
|
2913
|
+
} catch (err) {
|
|
2914
|
+
if (err instanceof MarketplaceUnavailableError) return {
|
|
2915
|
+
success: false,
|
|
2916
|
+
error: {
|
|
2917
|
+
code: "MARKETPLACE_UNAVAILABLE",
|
|
2918
|
+
message: "Marketplace is unavailable"
|
|
2919
|
+
}
|
|
2920
|
+
};
|
|
2921
|
+
console.error("Failed to check marketplace updates:", err);
|
|
2922
|
+
return {
|
|
2923
|
+
success: false,
|
|
2924
|
+
error: {
|
|
2925
|
+
code: "UPDATE_CHECK_FAILED",
|
|
2926
|
+
message: "Failed to check for updates"
|
|
2927
|
+
}
|
|
2928
|
+
};
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
async function handleMarketplaceSearch(marketplaceUrl, query, opts) {
|
|
2932
|
+
const client = getClient(marketplaceUrl);
|
|
2933
|
+
if (!client) return {
|
|
2934
|
+
success: false,
|
|
2935
|
+
error: {
|
|
2936
|
+
code: "MARKETPLACE_NOT_CONFIGURED",
|
|
2937
|
+
message: "Marketplace is not configured"
|
|
2938
|
+
}
|
|
2939
|
+
};
|
|
2940
|
+
try {
|
|
2941
|
+
return {
|
|
2942
|
+
success: true,
|
|
2943
|
+
data: await client.search(query, opts)
|
|
2944
|
+
};
|
|
2945
|
+
} catch (err) {
|
|
2946
|
+
if (err instanceof MarketplaceUnavailableError) return {
|
|
2947
|
+
success: false,
|
|
2948
|
+
error: {
|
|
2949
|
+
code: "MARKETPLACE_UNAVAILABLE",
|
|
2950
|
+
message: "Marketplace is unavailable"
|
|
2951
|
+
}
|
|
2952
|
+
};
|
|
2953
|
+
console.error("Failed to search marketplace:", err);
|
|
2954
|
+
return {
|
|
2955
|
+
success: false,
|
|
2956
|
+
error: {
|
|
2957
|
+
code: "SEARCH_FAILED",
|
|
2958
|
+
message: "Failed to search marketplace"
|
|
2959
|
+
}
|
|
2960
|
+
};
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
async function handleMarketplaceGetPlugin(marketplaceUrl, pluginId) {
|
|
2964
|
+
const client = getClient(marketplaceUrl);
|
|
2965
|
+
if (!client) return {
|
|
2966
|
+
success: false,
|
|
2967
|
+
error: {
|
|
2968
|
+
code: "MARKETPLACE_NOT_CONFIGURED",
|
|
2969
|
+
message: "Marketplace is not configured"
|
|
2970
|
+
}
|
|
2971
|
+
};
|
|
2972
|
+
try {
|
|
2973
|
+
return {
|
|
2974
|
+
success: true,
|
|
2975
|
+
data: await client.getPlugin(pluginId)
|
|
2976
|
+
};
|
|
2977
|
+
} catch (err) {
|
|
2978
|
+
if (err instanceof MarketplaceError && err.status === 404) return {
|
|
2979
|
+
success: false,
|
|
2980
|
+
error: {
|
|
2981
|
+
code: "NOT_FOUND",
|
|
2982
|
+
message: `Plugin not found: ${pluginId}`
|
|
2983
|
+
}
|
|
2984
|
+
};
|
|
2985
|
+
if (err instanceof MarketplaceUnavailableError) return {
|
|
2986
|
+
success: false,
|
|
2987
|
+
error: {
|
|
2988
|
+
code: "MARKETPLACE_UNAVAILABLE",
|
|
2989
|
+
message: "Marketplace is unavailable"
|
|
2990
|
+
}
|
|
2991
|
+
};
|
|
2992
|
+
console.error("Failed to get marketplace plugin:", err);
|
|
2993
|
+
return {
|
|
2994
|
+
success: false,
|
|
2995
|
+
error: {
|
|
2996
|
+
code: "GET_PLUGIN_FAILED",
|
|
2997
|
+
message: "Failed to get plugin details"
|
|
2998
|
+
}
|
|
2999
|
+
};
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
async function handleThemeSearch(marketplaceUrl, query, opts) {
|
|
3003
|
+
const client = getClient(marketplaceUrl);
|
|
3004
|
+
if (!client) return {
|
|
3005
|
+
success: false,
|
|
3006
|
+
error: {
|
|
3007
|
+
code: "MARKETPLACE_NOT_CONFIGURED",
|
|
3008
|
+
message: "Marketplace is not configured"
|
|
3009
|
+
}
|
|
3010
|
+
};
|
|
3011
|
+
try {
|
|
3012
|
+
return {
|
|
3013
|
+
success: true,
|
|
3014
|
+
data: await client.searchThemes(query, opts)
|
|
3015
|
+
};
|
|
3016
|
+
} catch (err) {
|
|
3017
|
+
if (err instanceof MarketplaceUnavailableError) return {
|
|
3018
|
+
success: false,
|
|
3019
|
+
error: {
|
|
3020
|
+
code: "MARKETPLACE_UNAVAILABLE",
|
|
3021
|
+
message: "Marketplace is unavailable"
|
|
3022
|
+
}
|
|
3023
|
+
};
|
|
3024
|
+
console.error("Failed to search themes:", err);
|
|
3025
|
+
return {
|
|
3026
|
+
success: false,
|
|
3027
|
+
error: {
|
|
3028
|
+
code: "THEME_SEARCH_FAILED",
|
|
3029
|
+
message: "Failed to search themes"
|
|
3030
|
+
}
|
|
3031
|
+
};
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
async function handleThemeGetDetail(marketplaceUrl, themeId) {
|
|
3035
|
+
const client = getClient(marketplaceUrl);
|
|
3036
|
+
if (!client) return {
|
|
3037
|
+
success: false,
|
|
3038
|
+
error: {
|
|
3039
|
+
code: "MARKETPLACE_NOT_CONFIGURED",
|
|
3040
|
+
message: "Marketplace is not configured"
|
|
3041
|
+
}
|
|
3042
|
+
};
|
|
3043
|
+
try {
|
|
3044
|
+
return {
|
|
3045
|
+
success: true,
|
|
3046
|
+
data: await client.getTheme(themeId)
|
|
3047
|
+
};
|
|
3048
|
+
} catch (err) {
|
|
3049
|
+
if (err instanceof MarketplaceError && err.status === 404) return {
|
|
3050
|
+
success: false,
|
|
3051
|
+
error: {
|
|
3052
|
+
code: "NOT_FOUND",
|
|
3053
|
+
message: `Theme not found: ${themeId}`
|
|
3054
|
+
}
|
|
3055
|
+
};
|
|
3056
|
+
if (err instanceof MarketplaceUnavailableError) return {
|
|
3057
|
+
success: false,
|
|
3058
|
+
error: {
|
|
3059
|
+
code: "MARKETPLACE_UNAVAILABLE",
|
|
3060
|
+
message: "Marketplace is unavailable"
|
|
3061
|
+
}
|
|
3062
|
+
};
|
|
3063
|
+
console.error("Failed to get marketplace theme:", err);
|
|
3064
|
+
return {
|
|
3065
|
+
success: false,
|
|
3066
|
+
error: {
|
|
3067
|
+
code: "GET_THEME_FAILED",
|
|
3068
|
+
message: "Failed to get theme details"
|
|
3069
|
+
}
|
|
3070
|
+
};
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
//#endregion
|
|
3075
|
+
//#region src/registry/config.ts
|
|
3076
|
+
/**
|
|
3077
|
+
* Canonicalize a capabilities list for set-style comparison.
|
|
3078
|
+
*
|
|
3079
|
+
* Capabilities (the legacy declared-access shape used by the current
|
|
3080
|
+
* sandbox enforcer) are conceptually a *set*: order, duplicates, and
|
|
3081
|
+
* non-string entries don't carry meaning. The install handler's drift
|
|
3082
|
+
* check compares the admin's acknowledged set against the bundle
|
|
3083
|
+
* manifest's set; both sides pass through this canonicalizer first so
|
|
3084
|
+
* an aggregator-supplied array with unstable order or junk entries
|
|
3085
|
+
* can't cause a spurious drift rejection.
|
|
3086
|
+
*
|
|
3087
|
+
* Filters non-strings, deduplicates, and sorts lexically. Named to
|
|
3088
|
+
* avoid shadowing `@emdash-cms/plugin-types`'s existing
|
|
3089
|
+
* `normalizeCapabilities` (which dedupes + applies the deprecated →
|
|
3090
|
+
* current alias map but does not filter junk or sort).
|
|
3091
|
+
*
|
|
3092
|
+
* Exported so the same shape is produced by the browser before sending
|
|
3093
|
+
* the `acknowledgedDeclaredAccess` payload and by the server before
|
|
3094
|
+
* comparing against the bundle.
|
|
3095
|
+
*/
|
|
3096
|
+
function canonicalCapabilitiesForDriftCheck(value) {
|
|
3097
|
+
if (!Array.isArray(value)) return [];
|
|
3098
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3099
|
+
for (const entry of value) if (typeof entry === "string" && entry.length > 0) seen.add(entry);
|
|
3100
|
+
return [...seen].toSorted();
|
|
3101
|
+
}
|
|
3102
|
+
/**
|
|
3103
|
+
* Returns whether a `(publisher_did, slug)` pair is on the
|
|
3104
|
+
* minimum-release-age exemption list. Exported so the same matcher is
|
|
3105
|
+
* used by the browser policy filter and the server-side install
|
|
3106
|
+
* enforcement.
|
|
3107
|
+
*
|
|
3108
|
+
* Matching is DID-only. Handles are aggregator-supplied envelope data
|
|
3109
|
+
* (mutable, controlled by an attacker who compromises the aggregator)
|
|
3110
|
+
* and cannot be used as a trust input -- a compromised aggregator
|
|
3111
|
+
* could claim any handle for any package and bypass the holdback. DIDs
|
|
3112
|
+
* are part of the AT URI of the package record and are independently
|
|
3113
|
+
* resolvable, so even a compromised aggregator can't lie about the
|
|
3114
|
+
* publisher DID without also breaking checksum verification downstream.
|
|
3115
|
+
*
|
|
3116
|
+
* Entries from config are already lowercased at manifest-build time.
|
|
3117
|
+
* Runtime values are lowercased here at compare time.
|
|
3118
|
+
*/
|
|
3119
|
+
function releaseExemptFromMinimumAge(exclude, publisherDid, slug) {
|
|
3120
|
+
if (!exclude || exclude.length === 0) return false;
|
|
3121
|
+
const didLower = publisherDid.toLowerCase();
|
|
3122
|
+
const fullDid = `${didLower}/${slug.toLowerCase()}`;
|
|
3123
|
+
for (const entry of exclude) {
|
|
3124
|
+
if (entry === didLower) return true;
|
|
3125
|
+
if (entry === fullDid) return true;
|
|
3126
|
+
}
|
|
3127
|
+
return false;
|
|
3128
|
+
}
|
|
3129
|
+
const DURATION_PATTERN = /^(\d+)(s|m|h|d|w)$/;
|
|
3130
|
+
/** Trailing slashes on the aggregator URL, stripped during normalization. */
|
|
3131
|
+
const TRAILING_SLASHES = /\/+$/;
|
|
3132
|
+
/** Trailing dot on a hostname, stripped before URL host comparisons. */
|
|
3133
|
+
const TRAILING_DOT$1 = /\.$/;
|
|
3134
|
+
/**
|
|
3135
|
+
* Parse a duration string or raw second count into a non-negative
|
|
3136
|
+
* integer count of seconds. Throws on unrecognised input so config
|
|
3137
|
+
* mistakes fail at startup rather than silently disabling the policy.
|
|
3138
|
+
*/
|
|
3139
|
+
function parseDurationSeconds(duration) {
|
|
3140
|
+
if (typeof duration === "number") {
|
|
3141
|
+
if (!Number.isFinite(duration) || duration < 0) throw new Error(`Invalid duration: ${duration} (must be a non-negative finite number)`);
|
|
3142
|
+
return Math.floor(duration);
|
|
3143
|
+
}
|
|
3144
|
+
const match = duration.match(DURATION_PATTERN);
|
|
3145
|
+
if (!match) throw new Error(`Invalid duration format: "${duration}". Use a duration string like "48h", "7d", "30m", or a number of seconds.`);
|
|
3146
|
+
const value = parseInt(match[1], 10);
|
|
3147
|
+
const unit = match[2];
|
|
3148
|
+
switch (unit) {
|
|
3149
|
+
case "s": return value;
|
|
3150
|
+
case "m": return value * 60;
|
|
3151
|
+
case "h": return value * 60 * 60;
|
|
3152
|
+
case "d": return value * 24 * 60 * 60;
|
|
3153
|
+
case "w": return value * 7 * 24 * 60 * 60;
|
|
3154
|
+
default: throw new Error(`Unknown duration unit: ${unit}`);
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
/**
|
|
3158
|
+
* Validate that `aggregatorUrl` is a safe outbound target for the
|
|
3159
|
+
* registry's XRPC calls. Same posture as artifact downloads: HTTPS
|
|
3160
|
+
* required in production; `http://localhost` allowed only in dev.
|
|
3161
|
+
*
|
|
3162
|
+
* The aggregator's responses are the trust source for release records,
|
|
3163
|
+
* checksums, labels, mirrors, and `indexedAt` (until full MST
|
|
3164
|
+
* verification lands). Allowing plain HTTP here would let a network
|
|
3165
|
+
* attacker swap a release record and point the artifact URL at their
|
|
3166
|
+
* own HTTPS bundle, defeating the checksum trust chain because the
|
|
3167
|
+
* attacker controls the unsigned transport that supplied the checksum.
|
|
3168
|
+
*/
|
|
3169
|
+
function validateAggregatorUrl(aggregatorUrl) {
|
|
3170
|
+
let parsed;
|
|
3171
|
+
try {
|
|
3172
|
+
parsed = new URL(aggregatorUrl);
|
|
3173
|
+
} catch {
|
|
3174
|
+
throw new Error(`registry.aggregatorUrl is not a valid URL: ${aggregatorUrl}`);
|
|
3175
|
+
}
|
|
3176
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error(`registry.aggregatorUrl must use http or https: ${aggregatorUrl}`);
|
|
3177
|
+
if (parsed.username || parsed.password) throw new Error("registry.aggregatorUrl must not contain embedded credentials (user:pass@)");
|
|
3178
|
+
const rawHostname = parsed.hostname.toLowerCase().replace(TRAILING_DOT$1, "");
|
|
3179
|
+
const hostname = rawHostname.startsWith("[") && rawHostname.endsWith("]") ? rawHostname.slice(1, -1) : rawHostname;
|
|
3180
|
+
const isLocalhost = hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "::1" || hostname.startsWith("::ffff:127.") || hostname.startsWith("::ffff:7f00:");
|
|
3181
|
+
if (!import.meta.env.DEV) {
|
|
3182
|
+
if (parsed.protocol === "http:") throw new Error(`registry.aggregatorUrl must use https in production: ${aggregatorUrl}`);
|
|
3183
|
+
if (isLocalhost) throw new Error(`registry.aggregatorUrl points at localhost; allowed only in dev: ${aggregatorUrl}`);
|
|
3184
|
+
} else if (parsed.protocol === "http:" && !isLocalhost) throw new Error(`registry.aggregatorUrl must use https (http allowed only for localhost in dev): ${aggregatorUrl}`);
|
|
3185
|
+
return parsed;
|
|
3186
|
+
}
|
|
3187
|
+
/**
|
|
3188
|
+
* Expand the `RegistryConfigInput` shorthand into the full
|
|
3189
|
+
* `RegistryConfig` object shape.
|
|
3190
|
+
*
|
|
3191
|
+
* Users can pass a bare aggregator URL string for the common case
|
|
3192
|
+
* (`experimental.registry: "https://registry.emdashcms.com"`); the
|
|
3193
|
+
* normalizer handles either form transparently.
|
|
3194
|
+
*
|
|
3195
|
+
* Returns `undefined` for `undefined` input so callers can chain with
|
|
3196
|
+
* optional chaining.
|
|
3197
|
+
*/
|
|
3198
|
+
function coerceRegistryConfig(input) {
|
|
3199
|
+
if (input === void 0) return void 0;
|
|
3200
|
+
if (typeof input === "string") return { aggregatorUrl: input };
|
|
3201
|
+
return input;
|
|
3202
|
+
}
|
|
3203
|
+
/**
|
|
3204
|
+
* Normalize the user-supplied `RegistryConfigInput` into the shape that
|
|
3205
|
+
* ships to the admin browser via the manifest endpoint.
|
|
3206
|
+
*
|
|
3207
|
+
* Accepts either the shorthand string form
|
|
3208
|
+
* (`"https://registry.emdashcms.com"`) or the full `RegistryConfig`
|
|
3209
|
+
* object. Returns `null` when `input` is undefined so callers can
|
|
3210
|
+
* spread the result directly into the manifest object.
|
|
3211
|
+
*
|
|
3212
|
+
* Throws if the aggregator URL is malformed, points at a forbidden host,
|
|
3213
|
+
* or `policy.minimumReleaseAge` is unparseable. These surface at
|
|
3214
|
+
* runtime startup as 500s from the manifest endpoint -- intended,
|
|
3215
|
+
* because the alternative is silently disabling the registry on
|
|
3216
|
+
* misconfigured sites.
|
|
3217
|
+
*
|
|
3218
|
+
* TODO: switch to a Zod schema for richer per-field error messages and
|
|
3219
|
+
* to surface misconfigurations to the admin UI as a banner instead of
|
|
3220
|
+
* a manifest 500.
|
|
3221
|
+
*/
|
|
3222
|
+
function normalizeRegistryConfig(input) {
|
|
3223
|
+
const config = coerceRegistryConfig(input);
|
|
3224
|
+
if (!config) return null;
|
|
3225
|
+
const aggregatorUrl = config.aggregatorUrl?.trim();
|
|
3226
|
+
if (!aggregatorUrl) throw new Error("registry.aggregatorUrl is required when registry is configured");
|
|
3227
|
+
validateAggregatorUrl(aggregatorUrl);
|
|
3228
|
+
const out = { aggregatorUrl: aggregatorUrl.replace(TRAILING_SLASHES, "") };
|
|
3229
|
+
if (config.acceptLabelers) out.acceptLabelers = config.acceptLabelers;
|
|
3230
|
+
const policy = {};
|
|
3231
|
+
let hasPolicy = false;
|
|
3232
|
+
if (config.policy?.minimumReleaseAge !== void 0) {
|
|
3233
|
+
policy.minimumReleaseAgeSeconds = parseDurationSeconds(config.policy.minimumReleaseAge);
|
|
3234
|
+
hasPolicy = true;
|
|
3235
|
+
}
|
|
3236
|
+
if (config.policy?.minimumReleaseAgeExclude !== void 0) {
|
|
3237
|
+
const list = config.policy.minimumReleaseAgeExclude.map((entry) => {
|
|
3238
|
+
const trimmed = entry.trim();
|
|
3239
|
+
if (!trimmed) throw new Error("registry.policy.minimumReleaseAgeExclude entries cannot be empty");
|
|
3240
|
+
return trimmed.toLowerCase();
|
|
3241
|
+
});
|
|
3242
|
+
if (list.length > 0) {
|
|
3243
|
+
policy.minimumReleaseAgeExclude = list;
|
|
3244
|
+
hasPolicy = true;
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
if (hasPolicy) out.policy = policy;
|
|
3248
|
+
return out;
|
|
3249
|
+
}
|
|
3250
|
+
|
|
3251
|
+
//#endregion
|
|
3252
|
+
//#region src/registry/plugin-id.ts
|
|
3253
|
+
/**
|
|
3254
|
+
* Plugin identifier helpers for the experimental decentralized plugin
|
|
3255
|
+
* registry.
|
|
3256
|
+
*
|
|
3257
|
+
* Registry plugins are addressed by `(publisher_did, slug)`, but the
|
|
3258
|
+
* EmDash runtime threads a single `pluginId: string` through every
|
|
3259
|
+
* install primitive (R2 storage keys, `PluginStateRepository`,
|
|
3260
|
+
* `syncMarketplacePlugins`, sandbox cache keys). Rather than refactor
|
|
3261
|
+
* everything to carry a composite identifier, we normalize the registry
|
|
3262
|
+
* tuple to an opaque content-addressed id that satisfies the existing
|
|
3263
|
+
* `validatePluginIdentifier` shape (`/^[a-z][a-z0-9_-]*$/`).
|
|
3264
|
+
*
|
|
3265
|
+
* The normalized id is:
|
|
3266
|
+
*
|
|
3267
|
+
* `r_` + base32-encoded SHA-256(publisher_did + "\n" + slug), truncated.
|
|
3268
|
+
*
|
|
3269
|
+
* Properties:
|
|
3270
|
+
*
|
|
3271
|
+
* - Deterministic. The same `(publisher, slug)` always produces the
|
|
3272
|
+
* same id, so re-resolving an installed plugin's metadata against
|
|
3273
|
+
* the aggregator is a straightforward lookup keyed by the columns
|
|
3274
|
+
* stored alongside `plugin_id` in `plugin_states`.
|
|
3275
|
+
* - Collision-resistant. 80 bits of truncated hash; a 50% birthday
|
|
3276
|
+
* collision happens around 2^40 distinct plugins, well beyond what
|
|
3277
|
+
* this registry will ever index.
|
|
3278
|
+
* - R2-safe. Lowercase alphanumerics + underscore (no hyphens), no
|
|
3279
|
+
* `:` or `/`. Existing sandbox cache keys (`${pluginId}:${version}`)
|
|
3280
|
+
* keep working because the id contains no `:`.
|
|
3281
|
+
* - Syntactically distinct from typical marketplace plugin ids: the
|
|
3282
|
+
* `r_` prefix plus exactly 16 base32 characters is unlikely to be
|
|
3283
|
+
* chosen as a marketplace id. Not formally guaranteed by the
|
|
3284
|
+
* validator -- marketplace ids may begin with `r_` and contain
|
|
3285
|
+
* hyphens -- so the install handler also performs an explicit
|
|
3286
|
+
* pre-existing-row check at the derived id and rejects any cross-
|
|
3287
|
+
* source collision (`PLUGIN_ID_COLLISION`).
|
|
3288
|
+
*
|
|
3289
|
+
* Reverse lookup (id → publisher + slug) requires the `plugin_states`
|
|
3290
|
+
* row -- the hash is one-way. That's intentional: any code path that
|
|
3291
|
+
* needs the human-meaningful pair already has the state row in hand.
|
|
3292
|
+
*/
|
|
3293
|
+
/** Length (in base32 characters) of the truncated hash portion of the id. */
|
|
3294
|
+
const HASH_LENGTH = 16;
|
|
3295
|
+
/** Total expected length of a registry plugin id. */
|
|
3296
|
+
const REGISTRY_PLUGIN_ID_LENGTH = 2 + HASH_LENGTH;
|
|
3297
|
+
const BASE32_ALPHABET = "abcdefghijklmnopqrstuvwxyz234567";
|
|
3298
|
+
/**
|
|
3299
|
+
* RFC 4648 base32 encoding without padding, lowercase. Implemented inline
|
|
3300
|
+
* rather than depending on a multibase library because (a) we only need
|
|
3301
|
+
* lowercase base32 here, (b) we need it to run identically in workerd,
|
|
3302
|
+
* Node, and the browser, and (c) the implementation is fewer lines than
|
|
3303
|
+
* the import statement would be.
|
|
3304
|
+
*/
|
|
3305
|
+
function base32Encode(bytes) {
|
|
3306
|
+
let bits = 0;
|
|
3307
|
+
let value = 0;
|
|
3308
|
+
let out = "";
|
|
3309
|
+
for (const byte of bytes) {
|
|
3310
|
+
value = value << 8 | byte;
|
|
3311
|
+
bits += 8;
|
|
3312
|
+
while (bits >= 5) {
|
|
3313
|
+
bits -= 5;
|
|
3314
|
+
out += BASE32_ALPHABET[value >>> bits & 31];
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
if (bits > 0) out += BASE32_ALPHABET[value << 5 - bits & 31];
|
|
3318
|
+
return out;
|
|
3319
|
+
}
|
|
3320
|
+
/**
|
|
3321
|
+
* Derive the normalized plugin id for a registry-published plugin.
|
|
3322
|
+
*
|
|
3323
|
+
* Throws if either input is empty or whitespace-only -- a missing DID
|
|
3324
|
+
* or slug is always a programming error in the install path, not a
|
|
3325
|
+
* recoverable runtime condition.
|
|
3326
|
+
*/
|
|
3327
|
+
async function makeRegistryPluginId(publisherDid, slug) {
|
|
3328
|
+
const did = publisherDid.trim();
|
|
3329
|
+
const s = slug.trim();
|
|
3330
|
+
if (!did) throw new Error("makeRegistryPluginId: publisherDid is required");
|
|
3331
|
+
if (!s) throw new Error("makeRegistryPluginId: slug is required");
|
|
3332
|
+
const input = `${did}\n${s}`;
|
|
3333
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input));
|
|
3334
|
+
return `r_${base32Encode(new Uint8Array(hashBuffer)).slice(0, HASH_LENGTH)}`;
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
//#endregion
|
|
3338
|
+
//#region src/api/handlers/registry.ts
|
|
3339
|
+
/** Matches a bare 64-character lowercase/uppercase hex SHA-256 digest. */
|
|
3340
|
+
const SHA256_HEX_PATTERN = /^[a-f0-9]{64}$/i;
|
|
3341
|
+
/** Compute the SHA-256 of `bytes` as a lowercase hex string. */
|
|
3342
|
+
async function sha256Hex(bytes) {
|
|
3343
|
+
const buf = await crypto.subtle.digest("SHA-256", bytes);
|
|
3344
|
+
const arr = new Uint8Array(buf);
|
|
3345
|
+
return Array.from(arr, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
3346
|
+
}
|
|
3347
|
+
/** multihash code for sha2-256 (single-byte varint). */
|
|
3348
|
+
const MULTIHASH_SHA256_CODE = 18;
|
|
3349
|
+
/** sha2-256 digest length in bytes (single-byte varint). */
|
|
3350
|
+
const MULTIHASH_SHA256_LENGTH = 32;
|
|
3351
|
+
/**
|
|
3352
|
+
* Compute the multibase-multihash sha2-256 checksum of `bytes`, in the
|
|
3353
|
+
* same `b<base32>` shape the registry CLI publishes
|
|
3354
|
+
* (`packages/plugin-cli/src/multihash.ts`). Returns a 56-character
|
|
3355
|
+
* string starting with `b`.
|
|
3356
|
+
*
|
|
3357
|
+
* The trust contract is: if both sides produce the same string for
|
|
3358
|
+
* the same bytes, the bytes are unchanged. We don't decode the
|
|
3359
|
+
* publisher-supplied checksum -- we just re-encode our own and compare,
|
|
3360
|
+
* which is equivalent and avoids needing a base32 decoder.
|
|
3361
|
+
*/
|
|
3362
|
+
async function sha256MultibaseMultihash(bytes) {
|
|
3363
|
+
const digestBuf = await crypto.subtle.digest("SHA-256", bytes);
|
|
3364
|
+
const digest = new Uint8Array(digestBuf);
|
|
3365
|
+
const multihash = new Uint8Array(2 + digest.length);
|
|
3366
|
+
multihash[0] = MULTIHASH_SHA256_CODE;
|
|
3367
|
+
multihash[1] = MULTIHASH_SHA256_LENGTH;
|
|
3368
|
+
multihash.set(digest, 2);
|
|
3369
|
+
const { toBase32 } = await import("@atcute/multibase");
|
|
3370
|
+
return `b${toBase32(multihash)}`;
|
|
3371
|
+
}
|
|
3372
|
+
/**
|
|
3373
|
+
* Verify that a checksum string from a release record's
|
|
3374
|
+
* `artifact.checksum` field corresponds to the SHA-256 of the given
|
|
3375
|
+
* bytes.
|
|
3376
|
+
*
|
|
3377
|
+
* Accepts two formats:
|
|
3378
|
+
*
|
|
3379
|
+
* - Bare lowercase/uppercase hex SHA-256 (64 chars). Convenience for
|
|
3380
|
+
* publishers / tools that emit hex rather than multibase.
|
|
3381
|
+
* - Multibase-multihash with the `b` (base32) prefix and sha2-256.
|
|
3382
|
+
* This is the format RFC 0001 mandates and the registry CLI emits
|
|
3383
|
+
* (see `packages/plugin-cli/src/multihash.ts`).
|
|
3384
|
+
*
|
|
3385
|
+
* Hash functions other than sha2-256 are out of scope for this
|
|
3386
|
+
* initial release; the install fails closed.
|
|
3387
|
+
*/
|
|
3388
|
+
async function verifyChecksum(bytes, checksum) {
|
|
3389
|
+
if (SHA256_HEX_PATTERN.test(checksum)) {
|
|
3390
|
+
const actual = await sha256Hex(bytes);
|
|
3391
|
+
return checksum.toLowerCase() === actual;
|
|
3392
|
+
}
|
|
3393
|
+
if (checksum.length === 56 && checksum.startsWith("b")) return (await sha256MultibaseMultihash(bytes)).toLowerCase() === checksum.toLowerCase();
|
|
3394
|
+
return false;
|
|
3395
|
+
}
|
|
3396
|
+
/**
|
|
3397
|
+
* Bytes-per-artifact cap on the gzipped tarball we'll download before
|
|
3398
|
+
* decompression. RFC 0001 caps a sandboxed plugin bundle at 256 KiB
|
|
3399
|
+
* decompressed (see `MAX_BUNDLE_SIZE` in cli/commands/bundle-utils.ts);
|
|
3400
|
+
* gzip on a mix of JSON manifest + JS code typically gives 0.3-0.6
|
|
3401
|
+
* ratio, so compressed bundles are well under 200 KiB in practice.
|
|
3402
|
+
* 512 KiB leaves margin for unusual file mixes that compress poorly
|
|
3403
|
+
* while still rejecting anything that's obviously not a legitimate
|
|
3404
|
+
* plugin bundle.
|
|
3405
|
+
*/
|
|
3406
|
+
const MAX_ARTIFACT_BYTES = 512 * 1024;
|
|
3407
|
+
/**
|
|
3408
|
+
* Maximum number of HTTP redirects followed during artifact download.
|
|
3409
|
+
* Each hop is independently URL-validated, so a malicious server cannot
|
|
3410
|
+
* redirect through a series of allowed-looking origins to reach a
|
|
3411
|
+
* forbidden one.
|
|
3412
|
+
*/
|
|
3413
|
+
const MAX_REDIRECTS = 5;
|
|
3414
|
+
/**
|
|
3415
|
+
* Wall-clock cap on any single artifact fetch attempt (per URL).
|
|
3416
|
+
* Defends against slow-loris mirrors that accept the connection but
|
|
3417
|
+
* never finish sending headers or body.
|
|
3418
|
+
*/
|
|
3419
|
+
const ARTIFACT_FETCH_TIMEOUT_MS = 15e3;
|
|
3420
|
+
/**
|
|
3421
|
+
* Total wall-clock budget for the artifact-download phase across all
|
|
3422
|
+
* mirrors and the declared URL. Even with the per-URL timeout, a
|
|
3423
|
+
* malicious mirror list could otherwise tie up the install request for
|
|
3424
|
+
* minutes; this caps total time at a budget interactive admins can
|
|
3425
|
+
* tolerate. Tuned so a fast happy path takes <1s of budget per
|
|
3426
|
+
* attempt and a worst case still completes in under a minute.
|
|
3427
|
+
*/
|
|
3428
|
+
const ARTIFACT_TOTAL_BUDGET_MS = 45e3;
|
|
3429
|
+
/**
|
|
3430
|
+
* Cap on the number of mirror URLs we try before falling back to the
|
|
3431
|
+
* publisher-declared URL. Matches the aggregator lexicon's
|
|
3432
|
+
* `mirrors` array length cap (16) but enforced here independently so
|
|
3433
|
+
* a misbehaving aggregator can't slow-loris us through hundreds of
|
|
3434
|
+
* URLs.
|
|
3435
|
+
*/
|
|
3436
|
+
const MAX_MIRRORS = 16;
|
|
3437
|
+
/**
|
|
3438
|
+
* Per-request timeout applied to every aggregator XRPC call
|
|
3439
|
+
* (`resolvePackage`, `getLatestRelease`, `listReleases`). Matches the
|
|
3440
|
+
* per-URL artifact-fetch cap. Without this, a slow-loris aggregator
|
|
3441
|
+
* can stall the install before the artifact phase even starts.
|
|
3442
|
+
*/
|
|
3443
|
+
const AGGREGATOR_REQUEST_TIMEOUT_MS = 15e3;
|
|
3444
|
+
/**
|
|
3445
|
+
* Total wall-clock budget for the aggregator-discovery phase
|
|
3446
|
+
* (resolve + selected-release lookup). Mirrors the artifact-download
|
|
3447
|
+
* budget. Worst case with the pinned-version path's 20-page cap is
|
|
3448
|
+
* 20 + 1 calls; capping the total ensures any one stalled call
|
|
3449
|
+
* still bounds the whole phase.
|
|
3450
|
+
*/
|
|
3451
|
+
const AGGREGATOR_TOTAL_BUDGET_MS = 3e4;
|
|
3452
|
+
/** Build a fetch function that enforces a per-request and per-budget timeout. */
|
|
3453
|
+
function timedFetch(totalDeadline) {
|
|
3454
|
+
return (input, init) => {
|
|
3455
|
+
const now = Date.now();
|
|
3456
|
+
const remaining = Math.max(0, totalDeadline - now);
|
|
3457
|
+
if (remaining === 0) return Promise.reject(/* @__PURE__ */ new Error("Aggregator request budget exhausted"));
|
|
3458
|
+
const timeout = Math.min(AGGREGATOR_REQUEST_TIMEOUT_MS, remaining);
|
|
3459
|
+
const controller = new AbortController();
|
|
3460
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
3461
|
+
const callerSignal = init?.signal;
|
|
3462
|
+
if (callerSignal) if (callerSignal.aborted) controller.abort(callerSignal.reason);
|
|
3463
|
+
else callerSignal.addEventListener("abort", () => controller.abort(callerSignal.reason));
|
|
3464
|
+
return fetch(input, {
|
|
3465
|
+
...init,
|
|
3466
|
+
signal: controller.signal
|
|
3467
|
+
}).finally(() => {
|
|
3468
|
+
clearTimeout(timer);
|
|
3469
|
+
});
|
|
3470
|
+
};
|
|
3471
|
+
}
|
|
3472
|
+
/**
|
|
3473
|
+
* Localhost-equivalent hostnames the artifact fetcher rejects in
|
|
3474
|
+
* production. The full literal-IP / DNS-rebinding blocklist lives in
|
|
3475
|
+
* `#security/ssrf.js` and is invoked via `resolveAndValidateExternalUrl`
|
|
3476
|
+
* below; this small set exists only because the artifact handler has
|
|
3477
|
+
* a dev-mode escape hatch that lets `http://localhost` through.
|
|
3478
|
+
*/
|
|
3479
|
+
const FORBIDDEN_HOSTNAMES = new Set([
|
|
3480
|
+
"localhost",
|
|
3481
|
+
"localhost.localdomain",
|
|
3482
|
+
"ip6-localhost",
|
|
3483
|
+
"ip6-loopback"
|
|
3484
|
+
]);
|
|
3485
|
+
/** Trailing dot on a hostname, stripped before URL host comparisons. */
|
|
3486
|
+
const TRAILING_DOT = /\.$/;
|
|
3487
|
+
/** Hostnames that resolve to the local machine; rejected outright in production. */
|
|
3488
|
+
function isLocalhostHostname(hostname) {
|
|
3489
|
+
const stripped = hostname.toLowerCase().replace(TRAILING_DOT, "");
|
|
3490
|
+
const h = stripped.startsWith("[") && stripped.endsWith("]") ? stripped.slice(1, -1) : stripped;
|
|
3491
|
+
if (FORBIDDEN_HOSTNAMES.has(h)) return true;
|
|
3492
|
+
if (h === "localhost") return true;
|
|
3493
|
+
if (h.endsWith(".localhost")) return true;
|
|
3494
|
+
if (h === "127.0.0.1" || h === "::1") return true;
|
|
3495
|
+
if (h.startsWith("::ffff:127.") || h.startsWith("::ffff:7f00:")) return true;
|
|
3496
|
+
return false;
|
|
3497
|
+
}
|
|
3498
|
+
/**
|
|
3499
|
+
* Validate that `urlString` is a safe outbound target for artifact
|
|
3500
|
+
* downloads. Rejects non-HTTPS (except localhost in dev), embedded
|
|
3501
|
+
* credentials, any host that's a loopback / private / link-local
|
|
3502
|
+
* literal address, and any hostname whose resolved A or AAAA records
|
|
3503
|
+
* point at one of those addresses (closes the DNS-rebinding gap).
|
|
3504
|
+
*
|
|
3505
|
+
* Wraps `resolveAndValidateExternalUrl` from the import-pipeline SSRF
|
|
3506
|
+
* module so both code paths share one DoH cache, one resolver, one
|
|
3507
|
+
* blocklist, and one set of regression tests. Layers an
|
|
3508
|
+
* artifact-specific protocol/dev-localhost policy on top.
|
|
3509
|
+
*
|
|
3510
|
+
* `import.meta.env.DEV` is a Vite/Astro compile-time constant, so
|
|
3511
|
+
* production bundles cannot enable the dev escape hatch at runtime.
|
|
3512
|
+
*/
|
|
3513
|
+
async function assertSafeArtifactUrl(urlString) {
|
|
3514
|
+
let url;
|
|
3515
|
+
try {
|
|
3516
|
+
url = new URL(urlString);
|
|
3517
|
+
} catch {
|
|
3518
|
+
throw new Error(`Invalid artifact URL: ${urlString}`);
|
|
3519
|
+
}
|
|
3520
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") throw new Error(`Artifact URL protocol not allowed: ${url.protocol}`);
|
|
3521
|
+
if (url.username || url.password) throw new Error("Artifact URL must not contain embedded credentials");
|
|
3522
|
+
const rawHostname = url.hostname.toLowerCase().replace(TRAILING_DOT, "");
|
|
3523
|
+
const hostname = rawHostname.startsWith("[") && rawHostname.endsWith("]") ? rawHostname.slice(1, -1) : rawHostname;
|
|
3524
|
+
const localhost = isLocalhostHostname(hostname);
|
|
3525
|
+
if (!import.meta.env.DEV) {
|
|
3526
|
+
if (url.protocol === "http:") throw new Error("Artifact URL must use https");
|
|
3527
|
+
if (localhost) throw new Error(`Artifact URL points to localhost: ${hostname}`);
|
|
3528
|
+
} else if (url.protocol === "http:" && !localhost) throw new Error("Artifact URL must use https (http allowed only for localhost in dev)");
|
|
3529
|
+
if (localhost) return url;
|
|
3530
|
+
try {
|
|
3531
|
+
return await resolveAndValidateExternalUrl(url.href);
|
|
3532
|
+
} catch (err) {
|
|
3533
|
+
if (err instanceof SsrfError) throw new Error(`Artifact URL rejected: ${err.message}`);
|
|
3534
|
+
throw err;
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3537
|
+
/**
|
|
3538
|
+
* Fetch one URL with manual redirect handling so every hop is
|
|
3539
|
+
* URL-validated, a hard byte cap so a malicious response body cannot
|
|
3540
|
+
* exhaust memory before the checksum check rejects it, and a wall-clock
|
|
3541
|
+
* timeout that covers connect, headers, and body together. The timeout
|
|
3542
|
+
* is the minimum of the per-URL cap and the remaining total budget so
|
|
3543
|
+
* a late-arriving mirror still respects the install's global budget.
|
|
3544
|
+
*/
|
|
3545
|
+
async function fetchWithLimits(initialUrl, totalDeadline) {
|
|
3546
|
+
const now = Date.now();
|
|
3547
|
+
const remaining = Math.max(0, totalDeadline - now);
|
|
3548
|
+
if (remaining === 0) throw new Error("Artifact download budget exhausted");
|
|
3549
|
+
const perUrlTimeout = Math.min(ARTIFACT_FETCH_TIMEOUT_MS, remaining);
|
|
3550
|
+
const controller = new AbortController();
|
|
3551
|
+
const timer = setTimeout(() => controller.abort(), perUrlTimeout);
|
|
3552
|
+
try {
|
|
3553
|
+
let current = await assertSafeArtifactUrl(initialUrl);
|
|
3554
|
+
let response;
|
|
3555
|
+
for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
|
|
3556
|
+
response = await fetch(current.href, {
|
|
3557
|
+
redirect: "manual",
|
|
3558
|
+
signal: controller.signal
|
|
3559
|
+
});
|
|
3560
|
+
if (response.status < 300 || response.status >= 400) break;
|
|
3561
|
+
const location = response.headers.get("location");
|
|
3562
|
+
if (!location) break;
|
|
3563
|
+
if (hop === MAX_REDIRECTS) throw new Error(`Too many redirects fetching artifact (>${MAX_REDIRECTS})`);
|
|
3564
|
+
current = await assertSafeArtifactUrl(new URL(location, current).href);
|
|
3565
|
+
}
|
|
3566
|
+
const finalResponse = response;
|
|
3567
|
+
if (!finalResponse.ok) throw new Error(`HTTP ${finalResponse.status}`);
|
|
3568
|
+
const lengthHeader = finalResponse.headers.get("content-length");
|
|
3569
|
+
if (lengthHeader) {
|
|
3570
|
+
const declared = Number(lengthHeader);
|
|
3571
|
+
if (Number.isFinite(declared) && declared > MAX_ARTIFACT_BYTES) throw new Error(`Artifact too large (declared ${declared} bytes, limit ${MAX_ARTIFACT_BYTES})`);
|
|
3572
|
+
}
|
|
3573
|
+
const body = finalResponse.body;
|
|
3574
|
+
if (!body) {
|
|
3575
|
+
const buf = new Uint8Array(await finalResponse.arrayBuffer());
|
|
3576
|
+
if (buf.byteLength > MAX_ARTIFACT_BYTES) throw new Error(`Artifact too large (limit ${MAX_ARTIFACT_BYTES} bytes)`);
|
|
3577
|
+
return buf;
|
|
3578
|
+
}
|
|
3579
|
+
const reader = body.getReader();
|
|
3580
|
+
const chunks = [];
|
|
3581
|
+
let total = 0;
|
|
3582
|
+
while (true) {
|
|
3583
|
+
const { done, value } = await reader.read();
|
|
3584
|
+
if (done) break;
|
|
3585
|
+
if (!value) continue;
|
|
3586
|
+
total += value.byteLength;
|
|
3587
|
+
if (total > MAX_ARTIFACT_BYTES) {
|
|
3588
|
+
try {
|
|
3589
|
+
await reader.cancel();
|
|
3590
|
+
} catch {}
|
|
3591
|
+
throw new Error(`Artifact too large (limit ${MAX_ARTIFACT_BYTES} bytes)`);
|
|
3592
|
+
}
|
|
3593
|
+
chunks.push(value);
|
|
3594
|
+
}
|
|
3595
|
+
const out = new Uint8Array(total);
|
|
3596
|
+
let offset = 0;
|
|
3597
|
+
for (const chunk of chunks) {
|
|
3598
|
+
out.set(chunk, offset);
|
|
3599
|
+
offset += chunk.byteLength;
|
|
3600
|
+
}
|
|
3601
|
+
return out;
|
|
3602
|
+
} finally {
|
|
3603
|
+
clearTimeout(timer);
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
/**
|
|
3607
|
+
* Strip query string and fragment from a URL for use in
|
|
3608
|
+
* client-visible error messages. Registry artifacts are often hosted
|
|
3609
|
+
* on storage backends that include presigned tokens in the query
|
|
3610
|
+
* string; surfacing the raw URL on a failed install leaks those
|
|
3611
|
+
* tokens into the admin's HTTP response and any log drain that
|
|
3612
|
+
* captures the error chain. Origin + pathname is enough to identify
|
|
3613
|
+
* the host and resource without exposing credentials.
|
|
3614
|
+
*
|
|
3615
|
+
* Falls back to a generic placeholder when the URL is malformed.
|
|
3616
|
+
*/
|
|
3617
|
+
function redactUrlForError(raw) {
|
|
3618
|
+
try {
|
|
3619
|
+
const u = new URL(raw);
|
|
3620
|
+
return `${u.origin}${u.pathname}`;
|
|
3621
|
+
} catch {
|
|
3622
|
+
return "<malformed url>";
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
/** Walk artifact source URLs in priority order and return the first that fetches successfully. */
|
|
3626
|
+
async function fetchArtifact(mirrors, declaredUrl) {
|
|
3627
|
+
const urls = [...mirrors.slice(0, MAX_MIRRORS), declaredUrl];
|
|
3628
|
+
const clientErrors = [];
|
|
3629
|
+
const totalDeadline = Date.now() + ARTIFACT_TOTAL_BUDGET_MS;
|
|
3630
|
+
for (const url of urls) {
|
|
3631
|
+
if (Date.now() >= totalDeadline) {
|
|
3632
|
+
clientErrors.push("(total artifact download budget exhausted)");
|
|
3633
|
+
break;
|
|
3634
|
+
}
|
|
3635
|
+
try {
|
|
3636
|
+
return await fetchWithLimits(url, totalDeadline);
|
|
3637
|
+
} catch (err) {
|
|
3638
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3639
|
+
console.warn(`[registry-install] Artifact fetch failed from ${url}:`, message);
|
|
3640
|
+
clientErrors.push(`${redactUrlForError(url)}: ${message}`);
|
|
3641
|
+
}
|
|
3642
|
+
}
|
|
3643
|
+
throw new Error(`Failed to download artifact from any source. Tried:\n ${clientErrors.join("\n ")}`);
|
|
3644
|
+
}
|
|
3645
|
+
async function handleRegistryInstall(db, storage, sandboxRunner, registryConfigInput, input, opts) {
|
|
3646
|
+
const registryConfig = coerceRegistryConfig(registryConfigInput);
|
|
3647
|
+
if (!registryConfig) return {
|
|
3648
|
+
success: false,
|
|
3649
|
+
error: {
|
|
3650
|
+
code: "REGISTRY_NOT_CONFIGURED",
|
|
3651
|
+
message: "Registry is not configured"
|
|
3652
|
+
}
|
|
3653
|
+
};
|
|
3654
|
+
if (!storage) return {
|
|
3655
|
+
success: false,
|
|
3656
|
+
error: {
|
|
3657
|
+
code: "STORAGE_NOT_CONFIGURED",
|
|
3658
|
+
message: "Storage is required for registry plugin installation"
|
|
3659
|
+
}
|
|
3660
|
+
};
|
|
3661
|
+
if (!sandboxRunner || !sandboxRunner.isAvailable()) return {
|
|
3662
|
+
success: false,
|
|
3663
|
+
error: {
|
|
3664
|
+
code: "SANDBOX_NOT_AVAILABLE",
|
|
3665
|
+
message: "Sandbox runner is required for registry plugins"
|
|
3666
|
+
}
|
|
3667
|
+
};
|
|
3668
|
+
try {
|
|
3669
|
+
validateAggregatorUrl(registryConfig.aggregatorUrl);
|
|
3670
|
+
} catch (err) {
|
|
3671
|
+
return {
|
|
3672
|
+
success: false,
|
|
3673
|
+
error: {
|
|
3674
|
+
code: "REGISTRY_NOT_CONFIGURED",
|
|
3675
|
+
message: err instanceof Error ? err.message : "Invalid aggregator URL"
|
|
3676
|
+
}
|
|
3677
|
+
};
|
|
3678
|
+
}
|
|
3679
|
+
const { did, slug, version: requestedVersion } = input;
|
|
3680
|
+
const { DiscoveryClient } = await import("@emdash-cms/registry-client/discovery");
|
|
3681
|
+
const aggregatorDeadline = Date.now() + AGGREGATOR_TOTAL_BUDGET_MS;
|
|
3682
|
+
const discovery = new DiscoveryClient({
|
|
3683
|
+
aggregatorUrl: registryConfig.aggregatorUrl,
|
|
3684
|
+
acceptLabelers: registryConfig.acceptLabelers,
|
|
3685
|
+
fetch: timedFetch(aggregatorDeadline)
|
|
3686
|
+
});
|
|
3687
|
+
if (!did.startsWith("did:") || did.split(":").length < 3) return {
|
|
3688
|
+
success: false,
|
|
3689
|
+
error: {
|
|
3690
|
+
code: "INVALID_DID",
|
|
3691
|
+
message: "DID must be a valid atproto DID (e.g. did:plc:abc123)"
|
|
3692
|
+
}
|
|
3693
|
+
};
|
|
3694
|
+
try {
|
|
3695
|
+
const publisherDid = did;
|
|
3696
|
+
const packageView = await discovery.getPackage({
|
|
3697
|
+
did: publisherDid,
|
|
3698
|
+
slug
|
|
3699
|
+
});
|
|
3700
|
+
const MAX_LIST_PAGES = 20;
|
|
3701
|
+
const releaseView = await (async () => {
|
|
3702
|
+
if (!requestedVersion) return discovery.getLatestRelease({
|
|
3703
|
+
did: publisherDid,
|
|
3704
|
+
package: slug
|
|
3705
|
+
});
|
|
3706
|
+
let cursor;
|
|
3707
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
3708
|
+
for (let page = 0; page < MAX_LIST_PAGES; page++) {
|
|
3709
|
+
if (cursor !== void 0) {
|
|
3710
|
+
if (seenCursors.has(cursor)) break;
|
|
3711
|
+
seenCursors.add(cursor);
|
|
3712
|
+
}
|
|
3713
|
+
const result = await discovery.listReleases({
|
|
3714
|
+
did: publisherDid,
|
|
3715
|
+
package: slug,
|
|
3716
|
+
cursor,
|
|
3717
|
+
limit: 50
|
|
3718
|
+
});
|
|
3719
|
+
for (const r of result.releases) if (r.version === requestedVersion) return r;
|
|
3720
|
+
if (!result.cursor) break;
|
|
3721
|
+
cursor = result.cursor;
|
|
3722
|
+
}
|
|
3723
|
+
})();
|
|
3724
|
+
if (!releaseView) return {
|
|
3725
|
+
success: false,
|
|
3726
|
+
error: {
|
|
3727
|
+
code: "NO_RELEASE",
|
|
3728
|
+
message: requestedVersion ? `Version ${requestedVersion} not found for ${publisherDid}/${slug}` : `No installable release found for ${publisherDid}/${slug}`
|
|
3729
|
+
}
|
|
3730
|
+
};
|
|
3731
|
+
const signedRelease = releaseView.release;
|
|
3732
|
+
if (packageView.did !== publisherDid || packageView.slug !== slug) return {
|
|
3733
|
+
success: false,
|
|
3734
|
+
error: {
|
|
3735
|
+
code: "AGGREGATOR_IDENTITY_MISMATCH",
|
|
3736
|
+
message: "Aggregator returned a package view for a different publisher or slug."
|
|
3737
|
+
}
|
|
3738
|
+
};
|
|
3739
|
+
if (releaseView.did !== publisherDid || releaseView.package !== slug || signedRelease?.package !== slug || requestedVersion !== void 0 && releaseView.version !== requestedVersion || signedRelease?.version !== releaseView.version) return {
|
|
3740
|
+
success: false,
|
|
3741
|
+
error: {
|
|
3742
|
+
code: "AGGREGATOR_IDENTITY_MISMATCH",
|
|
3743
|
+
message: "Aggregator returned a release view that does not match the requested package or version."
|
|
3744
|
+
}
|
|
3745
|
+
};
|
|
3746
|
+
const version = releaseView.version;
|
|
3747
|
+
const yanked = (packageView.labels ?? []).some((l) => l.val === "security:yanked");
|
|
3748
|
+
const releaseYanked = (releaseView.labels ?? []).some((l) => l.val === "security:yanked");
|
|
3749
|
+
if (yanked || releaseYanked) return {
|
|
3750
|
+
success: false,
|
|
3751
|
+
error: {
|
|
3752
|
+
code: "RELEASE_YANKED",
|
|
3753
|
+
message: "This release has been withdrawn (security:yanked label)."
|
|
3754
|
+
}
|
|
3755
|
+
};
|
|
3756
|
+
const minimumReleaseAge = registryConfig.policy?.minimumReleaseAge;
|
|
3757
|
+
let minimumReleaseAgeSeconds = 0;
|
|
3758
|
+
if (minimumReleaseAge !== void 0) try {
|
|
3759
|
+
minimumReleaseAgeSeconds = parseDurationSeconds(minimumReleaseAge);
|
|
3760
|
+
} catch (err) {
|
|
3761
|
+
return {
|
|
3762
|
+
success: false,
|
|
3763
|
+
error: {
|
|
3764
|
+
code: "REGISTRY_POLICY_INVALID",
|
|
3765
|
+
message: err instanceof Error ? err.message : "Invalid minimumReleaseAge value in registry config"
|
|
3766
|
+
}
|
|
3767
|
+
};
|
|
3768
|
+
}
|
|
3769
|
+
if (minimumReleaseAgeSeconds > 0) {
|
|
3770
|
+
const exclude = registryConfig.policy?.minimumReleaseAgeExclude?.map((e) => e.trim().toLowerCase());
|
|
3771
|
+
if (!releaseExemptFromMinimumAge(exclude, publisherDid, slug)) {
|
|
3772
|
+
const indexedAt = Date.parse(releaseView.indexedAt);
|
|
3773
|
+
if (!Number.isFinite(indexedAt)) return {
|
|
3774
|
+
success: false,
|
|
3775
|
+
error: {
|
|
3776
|
+
code: "RELEASE_TIMESTAMP_INVALID",
|
|
3777
|
+
message: "Release record is missing a valid indexed-at timestamp; cannot evaluate minimum release age policy."
|
|
3778
|
+
}
|
|
3779
|
+
};
|
|
3780
|
+
const ageSeconds = (Date.now() - indexedAt) / 1e3;
|
|
3781
|
+
if (ageSeconds < minimumReleaseAgeSeconds) {
|
|
3782
|
+
const remaining = Math.ceil(minimumReleaseAgeSeconds - ageSeconds);
|
|
3783
|
+
return {
|
|
3784
|
+
success: false,
|
|
3785
|
+
error: {
|
|
3786
|
+
code: "RELEASE_TOO_NEW",
|
|
3787
|
+
message: `This release does not meet the configured minimum release age of ${minimumReleaseAgeSeconds}s. It will be installable in ~${remaining}s.`
|
|
3788
|
+
}
|
|
3789
|
+
};
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3792
|
+
}
|
|
3793
|
+
const pluginId = await makeRegistryPluginId(publisherDid, slug);
|
|
3794
|
+
if (opts?.configuredPluginIds?.has(pluginId)) return {
|
|
3795
|
+
success: false,
|
|
3796
|
+
error: {
|
|
3797
|
+
code: "PLUGIN_ID_CONFLICT",
|
|
3798
|
+
message: "A configured plugin with the same derived id already exists"
|
|
3799
|
+
}
|
|
3800
|
+
};
|
|
3801
|
+
const stateRepo = new PluginStateRepository(db);
|
|
3802
|
+
const existing = await stateRepo.get(pluginId);
|
|
3803
|
+
if (existing) {
|
|
3804
|
+
if (existing.source === "registry") return {
|
|
3805
|
+
success: false,
|
|
3806
|
+
error: {
|
|
3807
|
+
code: "ALREADY_INSTALLED",
|
|
3808
|
+
message: `Plugin ${publisherDid}/${slug} is already installed`
|
|
3809
|
+
}
|
|
3810
|
+
};
|
|
3811
|
+
return {
|
|
3812
|
+
success: false,
|
|
3813
|
+
error: {
|
|
3814
|
+
code: "PLUGIN_ID_COLLISION",
|
|
3815
|
+
message: `A non-registry plugin already exists at the derived id ${pluginId}. Uninstall it before installing this registry plugin.`
|
|
3816
|
+
}
|
|
3817
|
+
};
|
|
3818
|
+
}
|
|
3819
|
+
const release = releaseView.release;
|
|
3820
|
+
const declaredUrl = release.artifacts?.package?.url;
|
|
3821
|
+
const declaredChecksum = release.artifacts?.package?.checksum;
|
|
3822
|
+
if (!declaredUrl || !declaredChecksum) return {
|
|
3823
|
+
success: false,
|
|
3824
|
+
error: {
|
|
3825
|
+
code: "INVALID_RELEASE",
|
|
3826
|
+
message: "Release record is missing artifact url or checksum"
|
|
3827
|
+
}
|
|
3828
|
+
};
|
|
3829
|
+
const artifactBytes = await fetchArtifact(releaseView.mirrors ?? [], declaredUrl);
|
|
3830
|
+
if (!await verifyChecksum(artifactBytes, declaredChecksum)) return {
|
|
3831
|
+
success: false,
|
|
3832
|
+
error: {
|
|
3833
|
+
code: "CHECKSUM_MISMATCH",
|
|
3834
|
+
message: "Artifact bytes do not match the release record's checksum, or the checksum encoding is unsupported."
|
|
3835
|
+
}
|
|
3836
|
+
};
|
|
3837
|
+
let bundle;
|
|
3838
|
+
try {
|
|
3839
|
+
bundle = await extractBundle(artifactBytes);
|
|
3840
|
+
} catch (err) {
|
|
3841
|
+
return {
|
|
3842
|
+
success: false,
|
|
3843
|
+
error: {
|
|
3844
|
+
code: "INVALID_BUNDLE",
|
|
3845
|
+
message: err instanceof Error ? err.message : "Failed to extract plugin bundle"
|
|
3846
|
+
}
|
|
3847
|
+
};
|
|
3848
|
+
}
|
|
3849
|
+
if (bundle.manifest.version !== version) return {
|
|
3850
|
+
success: false,
|
|
3851
|
+
error: {
|
|
3852
|
+
code: "MANIFEST_VERSION_MISMATCH",
|
|
3853
|
+
message: `Bundle manifest version (${bundle.manifest.version}) does not match release version (${version})`
|
|
3854
|
+
}
|
|
3855
|
+
};
|
|
3856
|
+
if (bundle.manifest.id !== slug) return {
|
|
3857
|
+
success: false,
|
|
3858
|
+
error: {
|
|
3859
|
+
code: "MANIFEST_ID_MISMATCH",
|
|
3860
|
+
message: `Bundle manifest id (${bundle.manifest.id}) does not match registry slug (${slug})`
|
|
3861
|
+
}
|
|
3862
|
+
};
|
|
3863
|
+
bundle.manifest = {
|
|
3864
|
+
...bundle.manifest,
|
|
3865
|
+
id: pluginId
|
|
3866
|
+
};
|
|
3867
|
+
const actualCapabilities = canonicalCapabilitiesForDriftCheck(bundle.manifest.capabilities);
|
|
3868
|
+
if (actualCapabilities.length > 0) {
|
|
3869
|
+
if (input.acknowledgedDeclaredAccess === void 0) return {
|
|
3870
|
+
success: false,
|
|
3871
|
+
error: {
|
|
3872
|
+
code: "DECLARED_ACCESS_REQUIRED",
|
|
3873
|
+
message: "This plugin declares capabilities that require consent. Re-open the install dialog to review and acknowledge them."
|
|
3874
|
+
}
|
|
3875
|
+
};
|
|
3876
|
+
const acknowledged = canonicalCapabilitiesForDriftCheck(input.acknowledgedDeclaredAccess);
|
|
3877
|
+
if (acknowledged.length !== actualCapabilities.length || acknowledged.some((cap, i) => cap !== actualCapabilities[i])) return {
|
|
3878
|
+
success: false,
|
|
3879
|
+
error: {
|
|
3880
|
+
code: "DECLARED_ACCESS_DRIFT",
|
|
3881
|
+
message: "Plugin manifest has changed since you consented. Re-open the install dialog to review the new permissions."
|
|
3882
|
+
}
|
|
3883
|
+
};
|
|
3884
|
+
}
|
|
3885
|
+
await storeBundleInR2(storage, pluginId, version, bundle, "registry");
|
|
3886
|
+
const profile = packageView.profile;
|
|
3887
|
+
try {
|
|
3888
|
+
await stateRepo.upsert(pluginId, version, "active", {
|
|
3889
|
+
source: "registry",
|
|
3890
|
+
displayName: profile.name ?? slug,
|
|
3891
|
+
description: profile.description ?? void 0,
|
|
3892
|
+
registryPublisherDid: publisherDid,
|
|
3893
|
+
registrySlug: slug
|
|
3894
|
+
});
|
|
3895
|
+
} catch (stateErr) {
|
|
3896
|
+
let lostRace = false;
|
|
3897
|
+
try {
|
|
3898
|
+
const winner = await stateRepo.get(pluginId);
|
|
3899
|
+
lostRace = winner !== void 0 && winner !== null;
|
|
3900
|
+
} catch (probeErr) {
|
|
3901
|
+
console.warn(`[registry-install] Failed to probe state row for ${pluginId} after state-write failure; treating as orphan:`, probeErr);
|
|
3902
|
+
}
|
|
3903
|
+
if (!lostRace) try {
|
|
3904
|
+
await deleteBundleFromR2(storage, pluginId, version, "registry");
|
|
3905
|
+
} catch (cleanupErr) {
|
|
3906
|
+
console.warn(`[registry-install] Failed to clean up R2 bundle for ${pluginId}@${version} after state-row write failure:`, cleanupErr);
|
|
3907
|
+
}
|
|
3908
|
+
throw stateErr;
|
|
3909
|
+
}
|
|
3910
|
+
return {
|
|
3911
|
+
success: true,
|
|
3912
|
+
data: {
|
|
3913
|
+
pluginId,
|
|
3914
|
+
publisherDid,
|
|
3915
|
+
slug,
|
|
3916
|
+
version,
|
|
3917
|
+
capabilities: bundle.manifest.capabilities
|
|
3918
|
+
}
|
|
3919
|
+
};
|
|
3920
|
+
} catch (err) {
|
|
3921
|
+
if (err instanceof EmDashStorageError) return {
|
|
3922
|
+
success: false,
|
|
3923
|
+
error: {
|
|
3924
|
+
code: err.code ?? "STORAGE_ERROR",
|
|
3925
|
+
message: "Storage error while installing plugin"
|
|
3926
|
+
}
|
|
3927
|
+
};
|
|
3928
|
+
console.error("[registry-install] Failed:", err);
|
|
3929
|
+
return {
|
|
3930
|
+
success: false,
|
|
3931
|
+
error: {
|
|
3932
|
+
code: "INSTALL_FAILED",
|
|
3933
|
+
message: err instanceof Error ? err.message : "Failed to install plugin from registry"
|
|
3934
|
+
}
|
|
3935
|
+
};
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3938
|
+
|
|
3939
|
+
//#endregion
|
|
3940
|
+
export { handleContentSchedule as $, handleMediaCreate as A, handleContentCountScheduled as B, handleSchemaCollectionUpdate as C, handleSchemaFieldList as D, handleSchemaFieldGet as E, handleRevisionGet as F, handleContentDuplicate as G, handleContentCreate as H, handleRevisionList as I, handleContentList as J, handleContentGet as K, handleRevisionRestore as L, handleMediaGet as M, handleMediaList as N, handleSchemaFieldReorder as O, handleMediaUpdate as P, handleContentRestore as Q, generateManifest as R, handleSchemaCollectionList as S, handleSchemaFieldDelete as T, handleContentDelete as U, handleContentCountTrashed as V, handleContentDiscardDraft as W, handleContentPermanentDelete as X, handleContentListTrashed as Y, handleContentPublish as Z, handleOrphanedTableList as _, handleMarketplaceSearch as a, handleSchemaCollectionDelete as b, handleMarketplaceUpdateCheck as c, loadBundleFromR2 as d, handleContentTranslations as et, handlePluginDisable as f, PluginStateRepository as g, handlePluginList as h, handleMarketplaceInstall as i, validateRev as it, handleMediaDelete as j, handleSchemaFieldUpdate as k, handleThemeGetDetail as l, handlePluginGet as m, normalizeRegistryConfig as n, handleContentUnschedule as nt, handleMarketplaceUninstall as o, handlePluginEnable as p, handleContentGetIncludingTrashed as q, handleMarketplaceGetPlugin as r, handleContentUpdate as rt, handleMarketplaceUpdate as s, handleRegistryInstall as t, handleContentUnpublish as tt, handleThemeSearch as u, handleOrphanedTableRegister as v, handleSchemaFieldCreate as w, handleSchemaCollectionGet as x, handleSchemaCollectionCreate as y, handleContentCompare as z };
|
|
3941
|
+
//# sourceMappingURL=api-ayIQ7rIe.mjs.map
|