emdash 0.39.0 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/action-token-DYwe4Pk0.mjs +26 -0
- package/dist/action-token-DYwe4Pk0.mjs.map +1 -0
- package/dist/activation-LOkDM5WT.mjs +382 -0
- package/dist/activation-LOkDM5WT.mjs.map +1 -0
- package/dist/adapters-B64LbYcB.d.mts +171 -0
- package/dist/adapters-B64LbYcB.d.mts.map +1 -0
- package/dist/allowed-origins-CHB8oZNM.mjs +116 -0
- package/dist/allowed-origins-CHB8oZNM.mjs.map +1 -0
- package/dist/api/route-utils.d.mts +4 -4
- package/dist/api/route-utils.mjs +49 -40
- package/dist/api/route-utils.mjs.map +1 -1
- package/dist/api/schemas/index.d.mts +2 -2
- package/dist/api/schemas/index.mjs +11 -7
- package/dist/api-cFFZpyS0.mjs +4718 -0
- package/dist/api-cFFZpyS0.mjs.map +1 -0
- package/dist/api-tokens-CE1ECIg0.mjs +3 -0
- package/dist/api-tokens-s4i4aUp_.mjs +166 -0
- package/dist/api-tokens-s4i4aUp_.mjs.map +1 -0
- package/dist/apply-CoT603H3.mjs +1163 -0
- package/dist/apply-CoT603H3.mjs.map +1 -0
- package/dist/approvals-CqMfNxak.mjs +460 -0
- package/dist/approvals-CqMfNxak.mjs.map +1 -0
- package/dist/artifact-fetch-DZMOGZ3-.mjs +818 -0
- package/dist/artifact-fetch-DZMOGZ3-.mjs.map +1 -0
- package/dist/astro/index.d.mts +14 -14
- package/dist/astro/index.mjs +62 -12
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +14 -12
- package/dist/astro/middleware/auth.d.mts.map +1 -1
- package/dist/astro/middleware/auth.mjs +26 -11
- package/dist/astro/middleware/auth.mjs.map +1 -1
- package/dist/astro/middleware/media-usage-write-fence.d.mts +9 -1
- package/dist/astro/middleware/media-usage-write-fence.d.mts.map +1 -1
- package/dist/astro/middleware/media-usage-write-fence.mjs +95 -13
- package/dist/astro/middleware/media-usage-write-fence.mjs.map +1 -1
- package/dist/astro/middleware/redirect.mjs +8 -8
- package/dist/astro/middleware/request-context.d.mts.map +1 -1
- package/dist/astro/middleware/request-context.mjs +145 -47
- package/dist/astro/middleware/request-context.mjs.map +1 -1
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.d.mts +12 -12
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +129 -122
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +14 -8
- package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs.map +1 -1
- package/dist/astro/routes/api/admin/allowed-domains/index.mjs +14 -8
- package/dist/astro/routes/api/admin/allowed-domains/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +5 -3
- package/dist/astro/routes/api/admin/api-tokens/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/admin/api-tokens/index.mjs +6 -4
- package/dist/astro/routes/api/admin/api-tokens/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +6 -4
- package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs.map +1 -1
- package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +17 -11
- package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs.map +1 -1
- package/dist/astro/routes/api/admin/byline-fields/index.mjs +17 -11
- package/dist/astro/routes/api/admin/byline-fields/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +17 -11
- package/dist/astro/routes/api/admin/byline-fields/reorder.mjs.map +1 -1
- package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +25 -19
- package/dist/astro/routes/api/admin/bylines/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +25 -19
- package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs.map +1 -1
- package/dist/astro/routes/api/admin/bylines/index.mjs +25 -19
- package/dist/astro/routes/api/admin/bylines/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/comments/_id_/status.mjs +19 -13
- package/dist/astro/routes/api/admin/comments/_id_/status.mjs.map +1 -1
- package/dist/astro/routes/api/admin/comments/_id_.mjs +7 -5
- package/dist/astro/routes/api/admin/comments/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/admin/comments/bulk.mjs +18 -12
- package/dist/astro/routes/api/admin/comments/bulk.mjs.map +1 -1
- package/dist/astro/routes/api/admin/comments/counts.mjs +7 -5
- package/dist/astro/routes/api/admin/comments/counts.mjs.map +1 -1
- package/dist/astro/routes/api/admin/comments/index.mjs +18 -12
- package/dist/astro/routes/api/admin/comments/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +6 -4
- package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs.map +1 -1
- package/dist/astro/routes/api/admin/hooks/exclusive/index.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +5 -6
- package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/media-usage/activation.mjs +21 -15
- package/dist/astro/routes/api/admin/media-usage/activation.mjs.map +1 -1
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.mjs +23 -17
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.mjs +23 -17
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.mjs.map +1 -1
- package/dist/astro/routes/api/admin/media-usage/progress.mjs +26 -15
- package/dist/astro/routes/api/admin/media-usage/progress.mjs.map +1 -1
- package/dist/astro/routes/api/admin/media-usage/repair.mjs +32 -19
- package/dist/astro/routes/api/admin/media-usage/repair.mjs.map +1 -1
- package/dist/astro/routes/api/admin/media-usage/work/index.mjs +23 -17
- package/dist/astro/routes/api/admin/media-usage/work/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/media-usage/work/retry.mjs +23 -17
- package/dist/astro/routes/api/admin/media-usage/work/retry.mjs.map +1 -1
- package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +5 -3
- package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/admin/oauth-clients/index.mjs +5 -3
- package/dist/astro/routes/api/admin/oauth-clients/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/disable.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +10 -8
- package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/enable.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +10 -8
- package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +72 -61
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/mcp.mjs +6 -4
- package/dist/astro/routes/api/admin/plugins/_id_/mcp.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs +71 -60
- package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +74 -63
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/update.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +84 -73
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/index.mjs +72 -61
- package/dist/astro/routes/api/admin/plugins/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +4 -2
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +71 -60
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +83 -72
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +71 -60
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +74 -63
- package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +85 -74
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +8 -6
- package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/install.mjs +76 -65
- package/dist/astro/routes/api/admin/plugins/registry/install.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/publisher-handle.d.mts +8 -0
- package/dist/astro/routes/api/admin/plugins/registry/publisher-handle.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/plugins/registry/publisher-handle.mjs +221 -0
- package/dist/astro/routes/api/admin/plugins/registry/publisher-handle.mjs.map +1 -0
- package/dist/astro/routes/api/admin/plugins/registry/verify.mjs +72 -61
- package/dist/astro/routes/api/admin/plugins/registry/verify.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/updates.mjs +71 -60
- package/dist/astro/routes/api/admin/plugins/updates.mjs.map +1 -1
- package/dist/astro/routes/api/admin/scheduled-policy-rejections/_collection_/_id_.mjs +6 -4
- package/dist/astro/routes/api/admin/scheduled-policy-rejections/_collection_/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +71 -60
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +4 -2
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs.map +1 -1
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +71 -60
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/transfer/_...path_.d.mts +12 -0
- package/dist/astro/routes/api/admin/transfer/_...path_.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/transfer/_...path_.mjs +144 -0
- package/dist/astro/routes/api/admin/transfer/_...path_.mjs.map +1 -0
- package/dist/astro/routes/api/admin/transfer/approvals/_id_/approve.d.mts +8 -0
- package/dist/astro/routes/api/admin/transfer/approvals/_id_/approve.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/transfer/approvals/_id_/approve.mjs +68 -0
- package/dist/astro/routes/api/admin/transfer/approvals/_id_/approve.mjs.map +1 -0
- package/dist/astro/routes/api/admin/transfer/approvals/_id_/deny.d.mts +8 -0
- package/dist/astro/routes/api/admin/transfer/approvals/_id_/deny.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/transfer/approvals/_id_/deny.mjs +68 -0
- package/dist/astro/routes/api/admin/transfer/approvals/_id_/deny.mjs.map +1 -0
- package/dist/astro/routes/api/admin/transfer/approvals/index.d.mts +8 -0
- package/dist/astro/routes/api/admin/transfer/approvals/index.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/transfer/approvals/index.mjs +67 -0
- package/dist/astro/routes/api/admin/transfer/approvals/index.mjs.map +1 -0
- package/dist/astro/routes/api/admin/transfer/capabilities.d.mts +8 -0
- package/dist/astro/routes/api/admin/transfer/capabilities.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/transfer/capabilities.mjs +70 -0
- package/dist/astro/routes/api/admin/transfer/capabilities.mjs.map +1 -0
- package/dist/astro/routes/api/admin/transfer/exports/_id_/advance.d.mts +8 -0
- package/dist/astro/routes/api/admin/transfer/exports/_id_/advance.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/transfer/exports/_id_/advance.mjs +67 -0
- package/dist/astro/routes/api/admin/transfer/exports/_id_/advance.mjs.map +1 -0
- package/dist/astro/routes/api/admin/transfer/exports/_id_/archive.d.mts +8 -0
- package/dist/astro/routes/api/admin/transfer/exports/_id_/archive.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/transfer/exports/_id_/archive.mjs +70 -0
- package/dist/astro/routes/api/admin/transfer/exports/_id_/archive.mjs.map +1 -0
- package/dist/astro/routes/api/admin/transfer/exports/_id_/files/_...path_.d.mts +8 -0
- package/dist/astro/routes/api/admin/transfer/exports/_id_/files/_...path_.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/transfer/exports/_id_/files/_...path_.mjs +69 -0
- package/dist/astro/routes/api/admin/transfer/exports/_id_/files/_...path_.mjs.map +1 -0
- package/dist/astro/routes/api/admin/transfer/exports/_id_/index.d.mts +8 -0
- package/dist/astro/routes/api/admin/transfer/exports/_id_/index.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/transfer/exports/_id_/index.mjs +66 -0
- package/dist/astro/routes/api/admin/transfer/exports/_id_/index.mjs.map +1 -0
- package/dist/astro/routes/api/admin/transfer/exports/_id_/manifest.d.mts +8 -0
- package/dist/astro/routes/api/admin/transfer/exports/_id_/manifest.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/transfer/exports/_id_/manifest.mjs +69 -0
- package/dist/astro/routes/api/admin/transfer/exports/_id_/manifest.mjs.map +1 -0
- package/dist/astro/routes/api/admin/transfer/exports/index.d.mts +9 -0
- package/dist/astro/routes/api/admin/transfer/exports/index.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/transfer/exports/index.mjs +91 -0
- package/dist/astro/routes/api/admin/transfer/exports/index.mjs.map +1 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/abandon.d.mts +8 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/abandon.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/abandon.mjs +66 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/abandon.mjs.map +1 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/advance.d.mts +8 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/advance.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/advance.mjs +70 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/advance.mjs.map +1 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/analyze.d.mts +8 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/analyze.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/analyze.mjs +78 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/analyze.mjs.map +1 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/cancel.d.mts +8 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/cancel.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/cancel.mjs +66 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/cancel.mjs.map +1 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/execute.d.mts +8 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/execute.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/execute.mjs +77 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/execute.mjs.map +1 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/files/_...path_.d.mts +8 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/files/_...path_.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/files/_...path_.mjs +77 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/files/_...path_.mjs.map +1 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/index.d.mts +8 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/index.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/index.mjs +66 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/index.mjs.map +1 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/missing.d.mts +8 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/missing.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/missing.mjs +70 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/missing.mjs.map +1 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/plan.d.mts +8 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/plan.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/plan.mjs +67 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/plan.mjs.map +1 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/receipt.d.mts +8 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/receipt.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/receipt.mjs +66 -0
- package/dist/astro/routes/api/admin/transfer/imports/_id_/receipt.mjs.map +1 -0
- package/dist/astro/routes/api/admin/transfer/imports/index.d.mts +9 -0
- package/dist/astro/routes/api/admin/transfer/imports/index.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/transfer/imports/index.mjs +99 -0
- package/dist/astro/routes/api/admin/transfer/imports/index.mjs.map +1 -0
- package/dist/astro/routes/api/admin/users/_id_/disable.mjs +3 -1
- package/dist/astro/routes/api/admin/users/_id_/disable.mjs.map +1 -1
- package/dist/astro/routes/api/admin/users/_id_/enable.mjs +3 -1
- package/dist/astro/routes/api/admin/users/_id_/enable.mjs.map +1 -1
- package/dist/astro/routes/api/admin/users/_id_/index.mjs +14 -8
- package/dist/astro/routes/api/admin/users/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/users/_id_/send-recovery.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +16 -6
- package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs.map +1 -1
- package/dist/astro/routes/api/admin/users/index.mjs +14 -8
- package/dist/astro/routes/api/admin/users/index.mjs.map +1 -1
- package/dist/astro/routes/api/auth/dev-bypass.mjs +5 -3
- package/dist/astro/routes/api/auth/dev-bypass.mjs.map +1 -1
- package/dist/astro/routes/api/auth/invite/accept.mjs +3 -1
- package/dist/astro/routes/api/auth/invite/accept.mjs.map +1 -1
- package/dist/astro/routes/api/auth/invite/complete.mjs +19 -13
- package/dist/astro/routes/api/auth/invite/complete.mjs.map +1 -1
- package/dist/astro/routes/api/auth/invite/index.d.mts.map +1 -1
- package/dist/astro/routes/api/auth/invite/index.mjs +24 -12
- package/dist/astro/routes/api/auth/invite/index.mjs.map +1 -1
- package/dist/astro/routes/api/auth/invite/register-options.mjs +18 -12
- package/dist/astro/routes/api/auth/invite/register-options.mjs.map +1 -1
- package/dist/astro/routes/api/auth/logout.mjs +4 -2
- package/dist/astro/routes/api/auth/logout.mjs.map +1 -1
- package/dist/astro/routes/api/auth/magic-link/send.d.mts.map +1 -1
- package/dist/astro/routes/api/auth/magic-link/send.mjs +29 -15
- package/dist/astro/routes/api/auth/magic-link/send.mjs.map +1 -1
- package/dist/astro/routes/api/auth/magic-link/verify.d.mts +2 -1
- package/dist/astro/routes/api/auth/magic-link/verify.d.mts.map +1 -1
- package/dist/astro/routes/api/auth/magic-link/verify.mjs +37 -17
- package/dist/astro/routes/api/auth/magic-link/verify.mjs.map +1 -1
- package/dist/astro/routes/api/auth/me.mjs +15 -9
- package/dist/astro/routes/api/auth/me.mjs.map +1 -1
- package/dist/astro/routes/api/auth/mode.mjs +4 -2
- package/dist/astro/routes/api/auth/mode.mjs.map +1 -1
- package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs +4 -4
- package/dist/astro/routes/api/auth/oauth/_provider_.mjs +2 -2
- package/dist/astro/routes/api/auth/passkey/_id_.mjs +14 -8
- package/dist/astro/routes/api/auth/passkey/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/auth/passkey/index.mjs +3 -1
- package/dist/astro/routes/api/auth/passkey/index.mjs.map +1 -1
- package/dist/astro/routes/api/auth/passkey/options.d.mts.map +1 -1
- package/dist/astro/routes/api/auth/passkey/options.mjs +21 -24
- package/dist/astro/routes/api/auth/passkey/options.mjs.map +1 -1
- package/dist/astro/routes/api/auth/passkey/register/options.mjs +18 -12
- package/dist/astro/routes/api/auth/passkey/register/options.mjs.map +1 -1
- package/dist/astro/routes/api/auth/passkey/register/verify.mjs +19 -13
- package/dist/astro/routes/api/auth/passkey/register/verify.mjs.map +1 -1
- package/dist/astro/routes/api/auth/passkey/verify.mjs +19 -13
- package/dist/astro/routes/api/auth/passkey/verify.mjs.map +1 -1
- package/dist/astro/routes/api/auth/signup/complete.mjs +19 -13
- package/dist/astro/routes/api/auth/signup/complete.mjs.map +1 -1
- package/dist/astro/routes/api/auth/signup/request.mjs +18 -12
- package/dist/astro/routes/api/auth/signup/request.mjs.map +1 -1
- package/dist/astro/routes/api/auth/signup/verify.mjs +3 -1
- package/dist/astro/routes/api/auth/signup/verify.mjs.map +1 -1
- package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +14 -11
- package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs.map +1 -1
- package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs +21 -15
- package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +4 -2
- package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +18 -13
- package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +4 -3
- package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/lock.mjs +18 -13
- package/dist/astro/routes/api/content/_collection_/_id_/lock.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +4 -2
- package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/plugin-extensions/_pluginId_/_kind_/_extensionId_.mjs +10 -8
- package/dist/astro/routes/api/content/_collection_/_id_/plugin-extensions/_pluginId_/_kind_/_extensionId_.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/preview-url.d.mts.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +68 -16
- package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +18 -13
- package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs +31 -22
- package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs +31 -22
- package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +4 -3
- package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +4 -2
- package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +18 -13
- package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.d.mts.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +38 -19
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +4 -2
- package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +18 -13
- package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_.mjs +18 -13
- package/dist/astro/routes/api/content/_collection_/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/authors.mjs +4 -2
- package/dist/astro/routes/api/content/_collection_/authors.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/index.mjs +15 -10
- package/dist/astro/routes/api/content/_collection_/index.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/trash.mjs +15 -9
- package/dist/astro/routes/api/content/_collection_/trash.mjs.map +1 -1
- package/dist/astro/routes/api/dashboard.mjs +11 -9
- package/dist/astro/routes/api/dashboard.mjs.map +1 -1
- package/dist/astro/routes/api/dev/emails.mjs +4 -2
- package/dist/astro/routes/api/dev/emails.mjs.map +1 -1
- package/dist/astro/routes/api/health.mjs +5 -3
- package/dist/astro/routes/api/health.mjs.map +1 -1
- package/dist/astro/routes/api/import/probe.d.mts +3 -3
- package/dist/astro/routes/api/import/probe.mjs +27 -21
- package/dist/astro/routes/api/import/probe.mjs.map +1 -1
- package/dist/astro/routes/api/import/wordpress/analyze.mjs +5 -3
- package/dist/astro/routes/api/import/wordpress/analyze.mjs.map +1 -1
- package/dist/astro/routes/api/import/wordpress/execute.d.mts +13 -13
- package/dist/astro/routes/api/import/wordpress/execute.mjs +18 -16
- package/dist/astro/routes/api/import/wordpress/execute.mjs.map +1 -1
- package/dist/astro/routes/api/import/wordpress/media.d.mts +12 -12
- package/dist/astro/routes/api/import/wordpress/media.mjs +19 -13
- package/dist/astro/routes/api/import/wordpress/media.mjs.map +1 -1
- package/dist/astro/routes/api/import/wordpress/prepare.mjs +18 -12
- package/dist/astro/routes/api/import/wordpress/prepare.mjs.map +1 -1
- package/dist/astro/routes/api/import/wordpress/rewrite-url-helpers.d.mts +5 -1
- package/dist/astro/routes/api/import/wordpress/rewrite-url-helpers.d.mts.map +1 -1
- package/dist/astro/routes/api/import/wordpress/rewrite-url-helpers.mjs +4 -2
- package/dist/astro/routes/api/import/wordpress/rewrite-url-helpers.mjs.map +1 -1
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts +13 -13
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +29 -15
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs.map +1 -1
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.d.mts +1 -1
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +27 -21
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs.map +1 -1
- package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +13 -13
- package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +38 -32
- package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs.map +1 -1
- package/dist/astro/routes/api/manifest.mjs +6 -4
- package/dist/astro/routes/api/manifest.mjs.map +1 -1
- package/dist/astro/routes/api/mcp.d.mts.map +1 -1
- package/dist/astro/routes/api/mcp.mjs +717 -67
- package/dist/astro/routes/api/mcp.mjs.map +1 -1
- package/dist/astro/routes/api/media/_id_/confirm.mjs +19 -13
- package/dist/astro/routes/api/media/_id_/confirm.mjs.map +1 -1
- package/dist/astro/routes/api/media/_id_/replace.mjs +16 -10
- package/dist/astro/routes/api/media/_id_/replace.mjs.map +1 -1
- package/dist/astro/routes/api/media/_id_/upload.mjs +8 -6
- package/dist/astro/routes/api/media/_id_/upload.mjs.map +1 -1
- package/dist/astro/routes/api/media/_id_/usage.mjs +34 -21
- package/dist/astro/routes/api/media/_id_/usage.mjs.map +1 -1
- package/dist/astro/routes/api/media/_id_.d.mts.map +1 -1
- package/dist/astro/routes/api/media/_id_.mjs +37 -26
- package/dist/astro/routes/api/media/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/media/asset/_id_/_filename_.mjs +5 -3
- package/dist/astro/routes/api/media/asset/_id_/_filename_.mjs.map +1 -1
- package/dist/astro/routes/api/media/file/_...key_.d.mts.map +1 -1
- package/dist/astro/routes/api/media/file/_...key_.mjs +31 -2
- package/dist/astro/routes/api/media/file/_...key_.mjs.map +1 -1
- package/dist/astro/routes/api/media/folders/_id_.mjs +16 -10
- package/dist/astro/routes/api/media/folders/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/media/folders/index.mjs +16 -10
- package/dist/astro/routes/api/media/folders/index.mjs.map +1 -1
- package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +4 -2
- package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs.map +1 -1
- package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +4 -2
- package/dist/astro/routes/api/media/providers/_providerId_/index.mjs.map +1 -1
- package/dist/astro/routes/api/media/providers/index.mjs +4 -2
- package/dist/astro/routes/api/media/providers/index.mjs.map +1 -1
- package/dist/astro/routes/api/media/upload-url.mjs +18 -12
- package/dist/astro/routes/api/media/upload-url.mjs.map +1 -1
- package/dist/astro/routes/api/media.mjs +36 -24
- package/dist/astro/routes/api/media.mjs.map +1 -1
- package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +18 -12
- package/dist/astro/routes/api/menus/_name_/items/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/menus/_name_/items.mjs +18 -12
- package/dist/astro/routes/api/menus/_name_/items.mjs.map +1 -1
- package/dist/astro/routes/api/menus/_name_/reorder.mjs +18 -12
- package/dist/astro/routes/api/menus/_name_/reorder.mjs.map +1 -1
- package/dist/astro/routes/api/menus/_name_/translations.mjs +18 -12
- package/dist/astro/routes/api/menus/_name_/translations.mjs.map +1 -1
- package/dist/astro/routes/api/menus/_name_.mjs +18 -12
- package/dist/astro/routes/api/menus/_name_.mjs.map +1 -1
- package/dist/astro/routes/api/menus/index.mjs +18 -12
- package/dist/astro/routes/api/menus/index.mjs.map +1 -1
- package/dist/astro/routes/api/oauth/authorize.d.mts.map +1 -1
- package/dist/astro/routes/api/oauth/authorize.mjs +9 -6
- package/dist/astro/routes/api/oauth/authorize.mjs.map +1 -1
- package/dist/astro/routes/api/oauth/device/authorize.mjs +7 -5
- package/dist/astro/routes/api/oauth/device/authorize.mjs.map +1 -1
- package/dist/astro/routes/api/oauth/device/code.mjs +10 -8
- package/dist/astro/routes/api/oauth/device/code.mjs.map +1 -1
- package/dist/astro/routes/api/oauth/device/token.mjs +9 -7
- package/dist/astro/routes/api/oauth/device/token.mjs.map +1 -1
- package/dist/astro/routes/api/oauth/register.mjs +4 -2
- package/dist/astro/routes/api/oauth/register.mjs.map +1 -1
- package/dist/astro/routes/api/oauth/token/refresh.mjs +7 -5
- package/dist/astro/routes/api/oauth/token/refresh.mjs.map +1 -1
- package/dist/astro/routes/api/oauth/token/revoke.mjs +7 -5
- package/dist/astro/routes/api/oauth/token/revoke.mjs.map +1 -1
- package/dist/astro/routes/api/oauth/token.mjs +7 -5
- package/dist/astro/routes/api/oauth/token.mjs.map +1 -1
- package/dist/astro/routes/api/openapi.json.mjs +497 -12
- package/dist/astro/routes/api/openapi.json.mjs.map +1 -1
- package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +10 -8
- package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs.map +1 -1
- package/dist/astro/routes/api/redirects/404s/index.mjs +21 -15
- package/dist/astro/routes/api/redirects/404s/index.mjs.map +1 -1
- package/dist/astro/routes/api/redirects/404s/summary.mjs +21 -15
- package/dist/astro/routes/api/redirects/404s/summary.mjs.map +1 -1
- package/dist/astro/routes/api/redirects/_id_.mjs +21 -15
- package/dist/astro/routes/api/redirects/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/redirects/index.mjs +21 -15
- package/dist/astro/routes/api/redirects/index.mjs.map +1 -1
- package/dist/astro/routes/api/relations/_id_/index.mjs +31 -22
- package/dist/astro/routes/api/relations/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/relations/_id_/translations.mjs +21 -16
- package/dist/astro/routes/api/relations/_id_/translations.mjs.map +1 -1
- package/dist/astro/routes/api/relations/index.mjs +31 -22
- package/dist/astro/routes/api/relations/index.mjs.map +1 -1
- package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +4 -2
- package/dist/astro/routes/api/revisions/_revisionId_/index.mjs.map +1 -1
- package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +4 -3
- package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs.map +1 -1
- package/dist/astro/routes/api/schema/block-types/_slug_/index.d.mts +9 -0
- package/dist/astro/routes/api/schema/block-types/_slug_/index.d.mts.map +1 -0
- package/dist/astro/routes/api/schema/block-types/_slug_/index.mjs +102 -0
- package/dist/astro/routes/api/schema/block-types/_slug_/index.mjs.map +1 -0
- package/dist/astro/routes/api/schema/block-types/_slug_/versions/_version_/activate.d.mts +8 -0
- package/dist/astro/routes/api/schema/block-types/_slug_/versions/_version_/activate.d.mts.map +1 -0
- package/dist/astro/routes/api/schema/block-types/_slug_/versions/_version_/activate.mjs +96 -0
- package/dist/astro/routes/api/schema/block-types/_slug_/versions/_version_/activate.mjs.map +1 -0
- package/dist/astro/routes/api/schema/block-types/index.d.mts +9 -0
- package/dist/astro/routes/api/schema/block-types/index.d.mts.map +1 -0
- package/dist/astro/routes/api/schema/block-types/index.mjs +102 -0
- package/dist/astro/routes/api/schema/block-types/index.mjs.map +1 -0
- package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +71 -60
- package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +71 -60
- package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +71 -60
- package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +71 -60
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/index.mjs +71 -60
- package/dist/astro/routes/api/schema/collections/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/reorder.mjs +71 -60
- package/dist/astro/routes/api/schema/collections/reorder.mjs.map +1 -1
- package/dist/astro/routes/api/schema/index.mjs +17 -14
- package/dist/astro/routes/api/schema/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs +71 -60
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs.map +1 -1
- package/dist/astro/routes/api/schema/orphans/index.mjs +71 -60
- package/dist/astro/routes/api/schema/orphans/index.mjs.map +1 -1
- package/dist/astro/routes/api/search/enable.mjs +20 -14
- package/dist/astro/routes/api/search/enable.mjs.map +1 -1
- package/dist/astro/routes/api/search/index.mjs +19 -13
- package/dist/astro/routes/api/search/index.mjs.map +1 -1
- package/dist/astro/routes/api/search/rebuild.mjs +20 -14
- package/dist/astro/routes/api/search/rebuild.mjs.map +1 -1
- package/dist/astro/routes/api/search/stats.mjs +10 -8
- package/dist/astro/routes/api/search/stats.mjs.map +1 -1
- package/dist/astro/routes/api/search/suggest.mjs +19 -13
- package/dist/astro/routes/api/search/suggest.mjs.map +1 -1
- package/dist/astro/routes/api/sections/_slug_.mjs +19 -13
- package/dist/astro/routes/api/sections/_slug_.mjs.map +1 -1
- package/dist/astro/routes/api/sections/index.mjs +19 -13
- package/dist/astro/routes/api/sections/index.mjs.map +1 -1
- package/dist/astro/routes/api/settings/backups/archives/_name_.mjs +8 -7
- package/dist/astro/routes/api/settings/backups/archives/_name_.mjs.map +1 -1
- package/dist/astro/routes/api/settings/backups/archives/index.mjs +8 -6
- package/dist/astro/routes/api/settings/backups/archives/index.mjs.map +1 -1
- package/dist/astro/routes/api/settings/backups/export.mjs +8 -7
- package/dist/astro/routes/api/settings/backups/export.mjs.map +1 -1
- package/dist/astro/routes/api/settings/backups/index.mjs +9 -8
- package/dist/astro/routes/api/settings/backups/index.mjs.map +1 -1
- package/dist/astro/routes/api/settings/email.mjs +6 -4
- package/dist/astro/routes/api/settings/email.mjs.map +1 -1
- package/dist/astro/routes/api/settings.mjs +25 -19
- package/dist/astro/routes/api/settings.mjs.map +1 -1
- package/dist/astro/routes/api/setup/admin-verify.mjs +20 -14
- package/dist/astro/routes/api/setup/admin-verify.mjs.map +1 -1
- package/dist/astro/routes/api/setup/admin.mjs +19 -13
- package/dist/astro/routes/api/setup/admin.mjs.map +1 -1
- package/dist/astro/routes/api/setup/dev-bypass.mjs +43 -37
- package/dist/astro/routes/api/setup/dev-bypass.mjs.map +1 -1
- package/dist/astro/routes/api/setup/dev-reset.mjs +4 -2
- package/dist/astro/routes/api/setup/dev-reset.mjs.map +1 -1
- package/dist/astro/routes/api/setup/index.mjs +48 -38
- package/dist/astro/routes/api/setup/index.mjs.map +1 -1
- package/dist/astro/routes/api/setup/status.mjs +5 -3
- package/dist/astro/routes/api/setup/status.mjs.map +1 -1
- package/dist/astro/routes/api/snapshot.mjs +10 -8
- package/dist/astro/routes/api/snapshot.mjs.map +1 -1
- package/dist/astro/routes/api/taxonomies/_name_/reorder.mjs +25 -19
- package/dist/astro/routes/api/taxonomies/_name_/reorder.mjs.map +1 -1
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +25 -19
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs.map +1 -1
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +25 -19
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs.map +1 -1
- package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +25 -19
- package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs.map +1 -1
- package/dist/astro/routes/api/taxonomies/_name_/translations.mjs +24 -18
- package/dist/astro/routes/api/taxonomies/_name_/translations.mjs.map +1 -1
- package/dist/astro/routes/api/taxonomies/_name_.mjs +25 -19
- package/dist/astro/routes/api/taxonomies/_name_.mjs.map +1 -1
- package/dist/astro/routes/api/taxonomies/bulk-tag.d.mts +8 -0
- package/dist/astro/routes/api/taxonomies/bulk-tag.d.mts.map +1 -0
- package/dist/astro/routes/api/taxonomies/bulk-tag.mjs +295 -0
- package/dist/astro/routes/api/taxonomies/bulk-tag.mjs.map +1 -0
- package/dist/astro/routes/api/taxonomies/index.mjs +25 -19
- package/dist/astro/routes/api/taxonomies/index.mjs.map +1 -1
- package/dist/astro/routes/api/themes/preview.mjs +7 -5
- package/dist/astro/routes/api/themes/preview.mjs.map +1 -1
- package/dist/astro/routes/api/typegen.d.mts.map +1 -1
- package/dist/astro/routes/api/typegen.mjs +12 -9
- package/dist/astro/routes/api/typegen.mjs.map +1 -1
- package/dist/astro/routes/api/visual-editing/action-token.mjs +7 -5
- package/dist/astro/routes/api/visual-editing/action-token.mjs.map +1 -1
- package/dist/astro/routes/api/visual-editing/content/_collection_/_id_/publish.mjs +22 -16
- package/dist/astro/routes/api/visual-editing/content/_collection_/_id_/publish.mjs.map +1 -1
- package/dist/astro/routes/api/visual-editing/toolbar-labels.mjs +3 -1
- package/dist/astro/routes/api/visual-editing/toolbar-labels.mjs.map +1 -1
- package/dist/astro/routes/api/well-known/auth.mjs +2 -2
- package/dist/astro/routes/api/well-known/oauth-authorization-server.mjs +2 -2
- package/dist/astro/routes/api/well-known/oauth-protected-resource.mjs +2 -2
- package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +16 -10
- package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs.map +1 -1
- package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +20 -14
- package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +20 -14
- package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs.map +1 -1
- package/dist/astro/routes/api/widget-areas/_name_.mjs +10 -8
- package/dist/astro/routes/api/widget-areas/_name_.mjs.map +1 -1
- package/dist/astro/routes/api/widget-areas/index.mjs +20 -14
- package/dist/astro/routes/api/widget-areas/index.mjs.map +1 -1
- package/dist/astro/routes/api/widget-components.mjs +4 -2
- package/dist/astro/routes/api/widget-components.mjs.map +1 -1
- package/dist/astro/routes/robots.txt.mjs +10 -10
- package/dist/astro/routes/sitemap-_collection_.xml.mjs +13 -13
- package/dist/astro/routes/sitemap.xml.mjs +11 -11
- package/dist/astro/types.d.mts +21 -17
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/audit-RfCugiSB.mjs +127 -0
- package/dist/audit-RfCugiSB.mjs.map +1 -0
- package/dist/auth/providers/github.d.mts +1 -1
- package/dist/auth/providers/google.d.mts +1 -1
- package/dist/authoritative-records-CoJvBXAD.mjs +5172 -0
- package/dist/authoritative-records-CoJvBXAD.mjs.map +1 -0
- package/dist/authorize-seCifOMJ.mjs +49 -0
- package/dist/authorize-seCifOMJ.mjs.map +1 -0
- package/dist/backup-CBu0LrXf.mjs +246 -0
- package/dist/backup-CBu0LrXf.mjs.map +1 -0
- package/dist/block-type-registry-DUI_3KxT.mjs +6709 -0
- package/dist/block-type-registry-DUI_3KxT.mjs.map +1 -0
- package/dist/block-types-BmZmLmAD.mjs +132 -0
- package/dist/block-types-BmZmLmAD.mjs.map +1 -0
- package/dist/block-values-C_VIPr5t.mjs +175 -0
- package/dist/block-values-C_VIPr5t.mjs.map +1 -0
- package/dist/byline-DYmkXqvo.mjs +909 -0
- package/dist/byline-DYmkXqvo.mjs.map +1 -0
- package/dist/byline-fields-D28AT-Fh.mjs +123 -0
- package/dist/byline-fields-D28AT-Fh.mjs.map +1 -0
- package/dist/byline-registry-gnKscbQX.mjs +406 -0
- package/dist/byline-registry-gnKscbQX.mjs.map +1 -0
- package/dist/bylines-BPwJqoYH.mjs +232 -0
- package/dist/bylines-BPwJqoYH.mjs.map +1 -0
- package/dist/bylines-ukLaYpVZ.mjs +214 -0
- package/dist/bylines-ukLaYpVZ.mjs.map +1 -0
- package/dist/cache-m7UEkRFV.mjs +108 -0
- package/dist/cache-m7UEkRFV.mjs.map +1 -0
- package/dist/canonical-YhEQezjg.mjs +174 -0
- package/dist/canonical-YhEQezjg.mjs.map +1 -0
- package/dist/capture-triggers-kJyl_yyM.mjs +362 -0
- package/dist/capture-triggers-kJyl_yyM.mjs.map +1 -0
- package/dist/challenge-store-ppU6Uv13.mjs +51 -0
- package/dist/challenge-store-ppU6Uv13.mjs.map +1 -0
- package/dist/checksum-B2IINIlN.mjs +162 -0
- package/dist/checksum-B2IINIlN.mjs.map +1 -0
- package/dist/chrome-tags-DApCQSIk.mjs +24 -0
- package/dist/chrome-tags-DApCQSIk.mjs.map +1 -0
- package/dist/chunks-Dms7Uxbt.mjs +25 -0
- package/dist/chunks-Dms7Uxbt.mjs.map +1 -0
- package/dist/cli/index.mjs +1471 -56
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/cf-access.d.mts +1 -1
- package/dist/client/index.d.mts +1050 -4
- package/dist/client/index.d.mts.map +1 -1
- package/dist/client/index.mjs +193 -11
- package/dist/client/index.mjs.map +1 -1
- package/dist/collection-deletion-BIm28mo3.mjs +436 -0
- package/dist/collection-deletion-BIm28mo3.mjs.map +1 -0
- package/dist/collection-deletion-processor-Br6rxs3u.mjs +217 -0
- package/dist/collection-deletion-processor-Br6rxs3u.mjs.map +1 -0
- package/dist/collection-slugs-cache-CjGk__zp.mjs +85 -0
- package/dist/collection-slugs-cache-CjGk__zp.mjs.map +1 -0
- package/dist/comment-CrBl22OF.mjs +313 -0
- package/dist/comment-CrBl22OF.mjs.map +1 -0
- package/dist/comment-reaction-JEQ2Fpd8.mjs +85 -0
- package/dist/comment-reaction-JEQ2Fpd8.mjs.map +1 -0
- package/dist/comments-2ah9du9y.mjs +195 -0
- package/dist/comments-2ah9du9y.mjs.map +1 -0
- package/dist/comments-fHZ8ON9O.mjs +99 -0
- package/dist/comments-fHZ8ON9O.mjs.map +1 -0
- package/dist/components-CXbq7CSM.mjs +108 -0
- package/dist/components-CXbq7CSM.mjs.map +1 -0
- package/dist/config-5ii2haYu.d.mts +6528 -0
- package/dist/config-5ii2haYu.d.mts.map +1 -0
- package/dist/config-CvmjJcVw.mjs +73 -0
- package/dist/config-CvmjJcVw.mjs.map +1 -0
- package/dist/config-IhybvkIQ.mjs +242 -0
- package/dist/config-IhybvkIQ.mjs.map +1 -0
- package/dist/config-Py43JK9r.d.mts +39 -0
- package/dist/config-Py43JK9r.d.mts.map +1 -0
- package/dist/content-DFaIEDj9.mjs +2281 -0
- package/dist/content-DFaIEDj9.mjs.map +1 -0
- package/dist/content-list-query-Dra4Dalc.d.mts +26 -0
- package/dist/content-list-query-Dra4Dalc.d.mts.map +1 -0
- package/dist/content-policy-DFNw91-d.mjs +46 -0
- package/dist/content-policy-DFNw91-d.mjs.map +1 -0
- package/dist/content-repair-Q8IUrpLL.mjs +442 -0
- package/dist/content-repair-Q8IUrpLL.mjs.map +1 -0
- package/dist/context-BeGccdUX.mjs +1921 -0
- package/dist/context-BeGccdUX.mjs.map +1 -0
- package/dist/cron-7DCJwPXq.mjs +284 -0
- package/dist/cron-7DCJwPXq.mjs.map +1 -0
- package/dist/dashboard-DjrB-xxZ.mjs +151 -0
- package/dist/dashboard-DjrB-xxZ.mjs.map +1 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +3 -3
- package/dist/db/libsql-migrations.d.mts +1 -1
- package/dist/db/libsql-migrations.mjs +6 -6
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/postgres-migrations.d.mts +1 -1
- package/dist/db/postgres-migrations.mjs +6 -6
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite-migrations.d.mts +1 -1
- package/dist/db/sqlite-migrations.mjs +6 -6
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/db/sqlite.mjs +2 -2
- package/dist/db/sqlite.mjs.map +1 -1
- package/dist/default-DQ6vsviu.mjs +81 -0
- package/dist/default-DQ6vsviu.mjs.map +1 -0
- package/dist/deferred-tasks-BPxXUB4x.d.mts +9 -0
- package/dist/deferred-tasks-BPxXUB4x.d.mts.map +1 -0
- package/dist/device-flow-CRd6DXmW.mjs +466 -0
- package/dist/device-flow-CRd6DXmW.mjs.map +1 -0
- package/dist/dialect-helpers-7_IqPdh9.mjs +307 -0
- package/dist/dialect-helpers-7_IqPdh9.mjs.map +1 -0
- package/dist/digest-BarSh-pN.mjs +194 -0
- package/dist/digest-BarSh-pN.mjs.map +1 -0
- package/dist/direct-executor-BmuFJJZV.mjs +104 -0
- package/dist/direct-executor-BmuFJJZV.mjs.map +1 -0
- package/dist/dist-BNwANFJR.mjs +810 -0
- package/dist/dist-BNwANFJR.mjs.map +1 -0
- package/dist/email-console-bJn3ctqA.mjs +50 -0
- package/dist/email-console-bJn3ctqA.mjs.map +1 -0
- package/dist/email-locale-CN6WAMx0.mjs +25 -0
- package/dist/email-locale-CN6WAMx0.mjs.map +1 -0
- package/dist/emdash-runtime-DAjghyEG.mjs +4186 -0
- package/dist/emdash-runtime-DAjghyEG.mjs.map +1 -0
- package/dist/enrich-Dl3kZ-lP.mjs +194 -0
- package/dist/enrich-Dl3kZ-lP.mjs.map +1 -0
- package/dist/entry-lock-BJYdBfXb.mjs +1097 -0
- package/dist/entry-lock-BJYdBfXb.mjs.map +1 -0
- package/dist/entry-lock-DHQOxN7R.mjs +163 -0
- package/dist/entry-lock-DHQOxN7R.mjs.map +1 -0
- package/dist/entry-lock-DVhCt49A.d.mts +4368 -0
- package/dist/entry-lock-DVhCt49A.d.mts.map +1 -0
- package/dist/entry-locks-D0foLdjQ.mjs +167 -0
- package/dist/entry-locks-D0foLdjQ.mjs.map +1 -0
- package/dist/error-CfvOHP0M.mjs +90 -0
- package/dist/error-CfvOHP0M.mjs.map +1 -0
- package/dist/errors-BF0_LFhW.mjs +460 -0
- package/dist/errors-BF0_LFhW.mjs.map +1 -0
- package/dist/errors-DgE1zJGG.mjs +120 -0
- package/dist/errors-DgE1zJGG.mjs.map +1 -0
- package/dist/escape-CuaZhZvL.mjs +9 -0
- package/dist/escape-CuaZhZvL.mjs.map +1 -0
- package/dist/fence-CMDXJYc_.mjs +162 -0
- package/dist/fence-CMDXJYc_.mjs.map +1 -0
- package/dist/field-defs-cache-HIgBbqZZ.mjs +77 -0
- package/dist/field-defs-cache-HIgBbqZZ.mjs.map +1 -0
- package/dist/fts-manager-S-K0laoS.mjs +443 -0
- package/dist/fts-manager-S-K0laoS.mjs.map +1 -0
- package/dist/gc-DjKY0Ay1.mjs +75 -0
- package/dist/gc-DjKY0Ay1.mjs.map +1 -0
- package/dist/hash-SDAW6zC-.mjs +37 -0
- package/dist/hash-SDAW6zC-.mjs.map +1 -0
- package/dist/hook-result-B5bXDgS2.d.mts +31 -0
- package/dist/hook-result-B5bXDgS2.d.mts.map +1 -0
- package/dist/hreflang-D5uNnsGJ.d.mts +50 -0
- package/dist/hreflang-D5uNnsGJ.d.mts.map +1 -0
- package/dist/hreflang-DMFBL2eb.mjs +105 -0
- package/dist/hreflang-DMFBL2eb.mjs.map +1 -0
- package/dist/http-route-dispatch-BitqzwMy.mjs +339 -0
- package/dist/http-route-dispatch-BitqzwMy.mjs.map +1 -0
- package/dist/identity-IyNWwXr6.mjs +35 -0
- package/dist/identity-IyNWwXr6.mjs.map +1 -0
- package/dist/import-C3JZplWq.mjs +2020 -0
- package/dist/import-C3JZplWq.mjs.map +1 -0
- package/dist/index-D-hdtLe8.d.mts +623 -0
- package/dist/index-D-hdtLe8.d.mts.map +1 -0
- package/dist/index.d.mts +24 -24
- package/dist/index.mjs +105 -93
- package/dist/install-finalization-DfM1fj5x.mjs +58 -0
- package/dist/install-finalization-DfM1fj5x.mjs.map +1 -0
- package/dist/keys-yxLm5l35.mjs +61 -0
- package/dist/keys-yxLm5l35.mjs.map +1 -0
- package/dist/kinds-CRu_T5T5.mjs +961 -0
- package/dist/kinds-CRu_T5T5.mjs.map +1 -0
- package/dist/lifecycle-BzeGPeQ-.mjs +25 -0
- package/dist/lifecycle-BzeGPeQ-.mjs.map +1 -0
- package/dist/load-XgJiE6_3.mjs +28 -0
- package/dist/load-XgJiE6_3.mjs.map +1 -0
- package/dist/loader-BzqeR1ZN.mjs +1086 -0
- package/dist/loader-BzqeR1ZN.mjs.map +1 -0
- package/dist/loops-BQSzv5Zn.mjs +203 -0
- package/dist/loops-BQSzv5Zn.mjs.map +1 -0
- package/dist/manifest-builder-B8zR-73J.mjs +189 -0
- package/dist/manifest-builder-B8zR-73J.mjs.map +1 -0
- package/dist/manifest-schema-CY3zng_M.mjs +492 -0
- package/dist/manifest-schema-CY3zng_M.mjs.map +1 -0
- package/dist/media/image-endpoint.mjs +1 -1
- package/dist/media/index.d.mts +1 -1
- package/dist/media/index.mjs +3 -3
- package/dist/media/local-runtime.d.mts +14 -14
- package/dist/media/local-runtime.d.mts.map +1 -1
- package/dist/media/local-runtime.mjs +15 -16
- package/dist/media/local-runtime.mjs.map +1 -1
- package/dist/media-Pu1L6xir.mjs +408 -0
- package/dist/media-Pu1L6xir.mjs.map +1 -0
- package/dist/media-Qp5S3eH3.mjs +355 -0
- package/dist/media-Qp5S3eH3.mjs.map +1 -0
- package/dist/media-allowlist-BSoa9Vvm.mjs +44 -0
- package/dist/media-allowlist-BSoa9Vvm.mjs.map +1 -0
- package/dist/media-folders-BE0oo2eX.mjs +210 -0
- package/dist/media-folders-BE0oo2eX.mjs.map +1 -0
- package/dist/media-h7tOiDLj.mjs +221 -0
- package/dist/media-h7tOiDLj.mjs.map +1 -0
- package/dist/media-url-jArBpzIG.mjs +43 -0
- package/dist/media-url-jArBpzIG.mjs.map +1 -0
- package/dist/media-usage-My-Vi-ex.mjs +1629 -0
- package/dist/media-usage-My-Vi-ex.mjs.map +1 -0
- package/dist/media-usage-activation-HKp7zk7W.mjs +78 -0
- package/dist/media-usage-activation-HKp7zk7W.mjs.map +1 -0
- package/dist/media-usage-work-C2OZLCAd.mjs +586 -0
- package/dist/media-usage-work-C2OZLCAd.mjs.map +1 -0
- package/dist/media-usage-work-DwUNbgvS.mjs +152 -0
- package/dist/media-usage-work-DwUNbgvS.mjs.map +1 -0
- package/dist/menus-BM7BPLOb.mjs +139 -0
- package/dist/menus-BM7BPLOb.mjs.map +1 -0
- package/dist/menus-ITeAbcIF.mjs +753 -0
- package/dist/menus-ITeAbcIF.mjs.map +1 -0
- package/dist/menus-TsSGmSR4.mjs +3608 -0
- package/dist/menus-TsSGmSR4.mjs.map +1 -0
- package/dist/migrations/index.d.mts +1 -1
- package/dist/migrations/index.mjs +5 -5
- package/dist/mime-ChLqYwDk.mjs +36 -0
- package/dist/mime-ChLqYwDk.mjs.map +1 -0
- package/dist/mode-CCuKXUci.mjs +23 -0
- package/dist/mode-CCuKXUci.mjs.map +1 -0
- package/dist/node-sqlite-compat-CONMsl-x.mjs +46 -0
- package/dist/node-sqlite-compat-CONMsl-x.mjs.map +1 -0
- package/dist/normalize-CO2P8-iE.mjs +191 -0
- package/dist/normalize-CO2P8-iE.mjs.map +1 -0
- package/dist/oauth-authorization-DlMIL5_h.mjs +274 -0
- package/dist/oauth-authorization-DlMIL5_h.mjs.map +1 -0
- package/dist/oauth-clients-Djq9r_KP.mjs +266 -0
- package/dist/oauth-clients-Djq9r_KP.mjs.map +1 -0
- package/dist/oauth-state-store-BbjnZXNC.mjs +50 -0
- package/dist/oauth-state-store-BbjnZXNC.mjs.map +1 -0
- package/dist/oauth-user-lookup-vue9a64A.mjs +26 -0
- package/dist/oauth-user-lookup-vue9a64A.mjs.map +1 -0
- package/dist/object-cache/memory.d.mts +1 -1
- package/dist/object-cache-CqWNY5uP.mjs +582 -0
- package/dist/object-cache-CqWNY5uP.mjs.map +1 -0
- package/dist/options-DhrI7eMv.mjs +208 -0
- package/dist/options-DhrI7eMv.mjs.map +1 -0
- package/dist/options-SQCHQCPH.d.mts +81 -0
- package/dist/options-SQCHQCPH.d.mts.map +1 -0
- package/dist/page/index.d.mts +2 -2
- package/dist/page/index.mjs +9 -9
- package/dist/parse-CVgO-Qai.d.mts +168 -0
- package/dist/parse-CVgO-Qai.d.mts.map +1 -0
- package/dist/parse-DY6OD1j_.mjs +81 -0
- package/dist/parse-DY6OD1j_.mjs.map +1 -0
- package/dist/passkey-config-X_H8128a.mjs +46 -0
- package/dist/passkey-config-X_H8128a.mjs.map +1 -0
- package/dist/paths-JxokKIbr.mjs +77 -0
- package/dist/paths-JxokKIbr.mjs.map +1 -0
- package/dist/patterns-BhAvqO0S.mjs +145 -0
- package/dist/patterns-BhAvqO0S.mjs.map +1 -0
- package/dist/placeholder-MSEd8gty.d.mts +306 -0
- package/dist/placeholder-MSEd8gty.d.mts.map +1 -0
- package/dist/plan-xjcWrAah.mjs +135 -0
- package/dist/plan-xjcWrAah.mjs.map +1 -0
- package/dist/plugin-test-runtime.d.mts +38 -15
- package/dist/plugin-test-runtime.d.mts.map +1 -1
- package/dist/plugin-test-runtime.mjs +112 -99
- package/dist/plugin-types.d.mts +2 -2
- package/dist/plugin-utils.d.mts +12 -12
- package/dist/plugins/adapt-sandbox-entry.d.mts +12 -12
- package/dist/plugins/adapt-sandbox-entry.mjs +3 -3
- package/dist/plugins/host.d.mts +5 -5
- package/dist/plugins/host.mjs +45 -37
- package/dist/plugins-DUXUt8B9.mjs +277 -0
- package/dist/plugins-DUXUt8B9.mjs.map +1 -0
- package/dist/policy-CUvwAARc.mjs +45 -0
- package/dist/policy-CUvwAARc.mjs.map +1 -0
- package/dist/portable-text-5O_ZGUjB.mjs +307 -0
- package/dist/portable-text-5O_ZGUjB.mjs.map +1 -0
- package/dist/preview-CIUcPYr6.mjs +107 -0
- package/dist/preview-CIUcPYr6.mjs.map +1 -0
- package/dist/protocol-B97DZaFr.d.mts +61 -0
- package/dist/protocol-B97DZaFr.d.mts.map +1 -0
- package/dist/public-submission-BFBUrqst.mjs +250 -0
- package/dist/public-submission-BFBUrqst.mjs.map +1 -0
- package/dist/public-url-Bn7U0D7-.mjs +101 -0
- package/dist/public-url-Bn7U0D7-.mjs.map +1 -0
- package/dist/query-B48peoRT.mjs +890 -0
- package/dist/query-B48peoRT.mjs.map +1 -0
- package/dist/query-C4IH0uhR.mjs +41 -0
- package/dist/query-C4IH0uhR.mjs.map +1 -0
- package/dist/rate-limit-BZ6nybgc.mjs +120 -0
- package/dist/rate-limit-BZ6nybgc.mjs.map +1 -0
- package/dist/receipt-B4wJj5Jq.mjs +43 -0
- package/dist/receipt-B4wJj5Jq.mjs.map +1 -0
- package/dist/redirect-Cd1E7VUt.mjs +17 -0
- package/dist/redirect-Cd1E7VUt.mjs.map +1 -0
- package/dist/redirect-iJf5fltD.mjs +415 -0
- package/dist/redirect-iJf5fltD.mjs.map +1 -0
- package/dist/redirects-CYu1u1aK.mjs +446 -0
- package/dist/redirects-CYu1u1aK.mjs.map +1 -0
- package/dist/redirects-Cv9apQKj.mjs +148 -0
- package/dist/redirects-Cv9apQKj.mjs.map +1 -0
- package/dist/registry/testing.mjs +3 -3
- package/dist/relation-M4NUCb0b.mjs +334 -0
- package/dist/relation-M4NUCb0b.mjs.map +1 -0
- package/dist/relations-CCif47Oq.mjs +403 -0
- package/dist/relations-CCif47Oq.mjs.map +1 -0
- package/dist/relations-CK9CYUyr.mjs +310 -0
- package/dist/relations-CK9CYUyr.mjs.map +1 -0
- package/dist/request-cache-DzLYKm_x.mjs +104 -0
- package/dist/request-cache-DzLYKm_x.mjs.map +1 -0
- package/dist/request-context.d.mts +1 -1
- package/dist/request-meta-sZXt3sRo.mjs +141 -0
- package/dist/request-meta-sZXt3sRo.mjs.map +1 -0
- package/dist/resolve-2WbQsCvT.mjs +199 -0
- package/dist/resolve-2WbQsCvT.mjs.map +1 -0
- package/dist/route-wire--1hLuACz.mjs +197 -0
- package/dist/route-wire--1hLuACz.mjs.map +1 -0
- package/dist/routes-CUsM40Cc.mjs +272 -0
- package/dist/routes-CUsM40Cc.mjs.map +1 -0
- package/dist/runner-1y4J9fJB.mjs +5742 -0
- package/dist/runner-1y4J9fJB.mjs.map +1 -0
- package/dist/runner-D5_LzKXI.d.mts +68 -0
- package/dist/runner-D5_LzKXI.d.mts.map +1 -0
- package/dist/runtime.d.mts +13 -13
- package/dist/runtime.mjs +4 -4
- package/dist/schema-BABiRdsH.mjs +13 -0
- package/dist/schema-BABiRdsH.mjs.map +1 -0
- package/dist/schema-Du3MzCG0.mjs +446 -0
- package/dist/schema-Du3MzCG0.mjs.map +1 -0
- package/dist/scopes-Dbk-ElhT.mjs +34 -0
- package/dist/scopes-Dbk-ElhT.mjs.map +1 -0
- package/dist/search-Bt1PL1jf.mjs +448 -0
- package/dist/search-Bt1PL1jf.mjs.map +1 -0
- package/dist/secrets-CIi0nFiR.mjs +341 -0
- package/dist/secrets-CIi0nFiR.mjs.map +1 -0
- package/dist/sections-Ddptx_o9.mjs +346 -0
- package/dist/sections-Ddptx_o9.mjs.map +1 -0
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +35 -30
- package/dist/seo/index.d.mts +3 -3
- package/dist/seo/index.mjs +25 -21
- package/dist/seo/index.mjs.map +1 -1
- package/dist/seo-CZT2oLtT.mjs +114 -0
- package/dist/seo-CZT2oLtT.mjs.map +1 -0
- package/dist/seo-D5w3-Azy.mjs +133 -0
- package/dist/seo-D5w3-Azy.mjs.map +1 -0
- package/dist/service-DuykXPyW.mjs +150 -0
- package/dist/service-DuykXPyW.mjs.map +1 -0
- package/dist/session-user-BhaVp8w0.mjs +51 -0
- package/dist/session-user-BhaVp8w0.mjs.map +1 -0
- package/dist/settings-2xNYr90o.mjs +51 -0
- package/dist/settings-2xNYr90o.mjs.map +1 -0
- package/dist/settings-CgjRWGW9.mjs +439 -0
- package/dist/settings-CgjRWGW9.mjs.map +1 -0
- package/dist/settings-DYidIB26.mjs +61 -0
- package/dist/settings-DYidIB26.mjs.map +1 -0
- package/dist/setup-complete-bOogcHWG.mjs +26 -0
- package/dist/setup-complete-bOogcHWG.mjs.map +1 -0
- package/dist/setup-nonce-0EGuKM4s.mjs +25 -0
- package/dist/setup-nonce-0EGuKM4s.mjs.map +1 -0
- package/dist/site-settings-DOgRUoFX.mjs +50 -0
- package/dist/site-settings-DOgRUoFX.mjs.map +1 -0
- package/dist/site-url-BU_bbxwj.mjs +16 -0
- package/dist/site-url-BU_bbxwj.mjs.map +1 -0
- package/dist/snapshot-CugwUqKQ.mjs +237 -0
- package/dist/snapshot-CugwUqKQ.mjs.map +1 -0
- package/dist/ssrf-CJUnJymJ.mjs +406 -0
- package/dist/ssrf-CJUnJymJ.mjs.map +1 -0
- package/dist/stage-DgrBz_du.mjs +662 -0
- package/dist/stage-DgrBz_du.mjs.map +1 -0
- package/dist/state-DDiPySSG.mjs +153 -0
- package/dist/state-DDiPySSG.mjs.map +1 -0
- package/dist/status-DZKIG4a9.mjs +30 -0
- package/dist/status-DZKIG4a9.mjs.map +1 -0
- package/dist/storage/local.d.mts +1 -1
- package/dist/storage/local.mjs +1 -1
- package/dist/storage/s3.d.mts +1 -1
- package/dist/storage/s3.mjs +1 -1
- package/dist/storage-query-Bdv7A62k.d.mts +1576 -0
- package/dist/storage-query-Bdv7A62k.d.mts.map +1 -0
- package/dist/tar-_1AgTYFu.mjs +449 -0
- package/dist/tar-_1AgTYFu.mjs.map +1 -0
- package/dist/target-_7pn2Gbg.mjs +27 -0
- package/dist/target-_7pn2Gbg.mjs.map +1 -0
- package/dist/taxonomies-C1IwNe8Q.mjs +697 -0
- package/dist/taxonomies-C1IwNe8Q.mjs.map +1 -0
- package/dist/taxonomies-C_XMoRcX.mjs +939 -0
- package/dist/taxonomies-C_XMoRcX.mjs.map +1 -0
- package/dist/taxonomy-C3Fu1YeH.mjs +525 -0
- package/dist/taxonomy-C3Fu1YeH.mjs.map +1 -0
- package/dist/tokens-u78jETkX.mjs +171 -0
- package/dist/tokens-u78jETkX.mjs.map +1 -0
- package/dist/transfer-BhUCrgDc.mjs +8675 -0
- package/dist/transfer-BhUCrgDc.mjs.map +1 -0
- package/dist/transfer-DIGvR9Af.mjs +166 -0
- package/dist/transfer-DIGvR9Af.mjs.map +1 -0
- package/dist/transfer-download-Pml7RMio.mjs +33 -0
- package/dist/transfer-download-Pml7RMio.mjs.map +1 -0
- package/dist/transformations-9gfpwY65.mjs +196 -0
- package/dist/transformations-9gfpwY65.mjs.map +1 -0
- package/dist/transport-BIfH7_hx.d.mts +42 -0
- package/dist/transport-BIfH7_hx.d.mts.map +1 -0
- package/dist/transport-DKWIq53q.mjs +136 -0
- package/dist/transport-DKWIq53q.mjs.map +1 -0
- package/dist/trusted-proxy-tKiVtyTD.mjs +51 -0
- package/dist/trusted-proxy-tKiVtyTD.mjs.map +1 -0
- package/dist/turnstile-DpsoyOfg.mjs +53 -0
- package/dist/turnstile-DpsoyOfg.mjs.map +1 -0
- package/dist/types-B4sUC2p1.d.mts +198 -0
- package/dist/types-B4sUC2p1.d.mts.map +1 -0
- package/dist/types-B7Lmsh1E.d.mts +887 -0
- package/dist/types-B7Lmsh1E.d.mts.map +1 -0
- package/dist/types-BZakh7Ym.mjs +22 -0
- package/dist/types-BZakh7Ym.mjs.map +1 -0
- package/dist/types-B_CppEmv.d.mts +264 -0
- package/dist/types-B_CppEmv.d.mts.map +1 -0
- package/dist/types-BcrBrE6-.mjs +159 -0
- package/dist/types-BcrBrE6-.mjs.map +1 -0
- package/dist/types-CAZgq9fO.d.mts +124 -0
- package/dist/types-CAZgq9fO.d.mts.map +1 -0
- package/dist/types-Cd-O03VG.d.mts +363 -0
- package/dist/types-Cd-O03VG.d.mts.map +1 -0
- package/dist/types-Cr-Kg-3D.d.mts +1641 -0
- package/dist/types-Cr-Kg-3D.d.mts.map +1 -0
- package/dist/types-D2fvWd1i.d.mts +359 -0
- package/dist/types-D2fvWd1i.d.mts.map +1 -0
- package/dist/types-D7HUsguf.mjs +77 -0
- package/dist/types-D7HUsguf.mjs.map +1 -0
- package/dist/types-DA1Te3Oy.d.mts +198 -0
- package/dist/types-DA1Te3Oy.d.mts.map +1 -0
- package/dist/types-DhV6Q5bP.mjs +18 -0
- package/dist/types-DhV6Q5bP.mjs.map +1 -0
- package/dist/types-DnnVSl6Y.d.mts +85 -0
- package/dist/types-DnnVSl6Y.d.mts.map +1 -0
- package/dist/types-VT4-BugW.mjs +16 -0
- package/dist/types-VT4-BugW.mjs.map +1 -0
- package/dist/upload-attempts-B68ZGNu9.mjs +32 -0
- package/dist/upload-attempts-B68ZGNu9.mjs.map +1 -0
- package/dist/url-pattern-CCeZMGOh.mjs +33 -0
- package/dist/url-pattern-CCeZMGOh.mjs.map +1 -0
- package/dist/user-DDSL-sGw.mjs +171 -0
- package/dist/user-DDSL-sGw.mjs.map +1 -0
- package/dist/utils-DdhmO9w9.mjs +439 -0
- package/dist/utils-DdhmO9w9.mjs.map +1 -0
- package/dist/validate-CHosPEhs.mjs +453 -0
- package/dist/validate-CHosPEhs.mjs.map +1 -0
- package/dist/validate-CM79QXYE.d.mts +432 -0
- package/dist/validate-CM79QXYE.d.mts.map +1 -0
- package/dist/validation-BFecQczG.mjs +218 -0
- package/dist/validation-BFecQczG.mjs.map +1 -0
- package/dist/version-BRZXVifl.mjs +9 -0
- package/dist/version-BRZXVifl.mjs.map +1 -0
- package/dist/version-DUQDHgrf.mjs +7 -0
- package/dist/version-DUQDHgrf.mjs.map +1 -0
- package/dist/widgets-WdV72brA.mjs +126 -0
- package/dist/widgets-WdV72brA.mjs.map +1 -0
- package/dist/wxr-taxonomies-BHTdqkmZ.d.mts +34 -0
- package/dist/wxr-taxonomies-BHTdqkmZ.d.mts.map +1 -0
- package/dist/wxr-taxonomies-DP6ESgMN.mjs +372 -0
- package/dist/wxr-taxonomies-DP6ESgMN.mjs.map +1 -0
- package/dist/zod-generator-D6uE4WiS.mjs +389 -0
- package/dist/zod-generator-D6uE4WiS.mjs.map +1 -0
- package/package.json +6 -5
- package/src/api/authorize.ts +17 -0
- package/src/api/email-locale.ts +24 -0
- package/src/api/errors.ts +16 -0
- package/src/api/handlers/api-tokens.ts +18 -3
- package/src/api/handlers/block-types.ts +129 -0
- package/src/api/handlers/bulk-tag.ts +309 -0
- package/src/api/handlers/comments.ts +0 -28
- package/src/api/handlers/index.ts +10 -0
- package/src/api/handlers/manifest.ts +8 -1
- package/src/api/handlers/marketplace.ts +22 -16
- package/src/api/handlers/media-usage.ts +31 -12
- package/src/api/handlers/media.ts +19 -14
- package/src/api/handlers/schema.ts +33 -8
- package/src/api/handlers/settings.ts +2 -2
- package/src/api/handlers/snapshot.ts +4 -0
- package/src/api/handlers/taxonomies.ts +132 -83
- package/src/api/handlers/transfer.ts +1347 -0
- package/src/api/handlers/validate-media-fields.ts +124 -12
- package/src/api/media-usage-write-fence.ts +5 -26
- package/src/api/openapi/document.ts +614 -5
- package/src/api/schemas/auth.ts +7 -1
- package/src/api/schemas/common.ts +5 -0
- package/src/api/schemas/content.ts +20 -1
- package/src/api/schemas/media-usage.ts +11 -0
- package/src/api/schemas/menus.ts +1 -1
- package/src/api/schemas/schema.ts +117 -0
- package/src/api/schemas/settings.ts +3 -3
- package/src/api/schemas/taxonomies.ts +18 -0
- package/src/api/schemas/transfer.ts +249 -0
- package/src/api/transfer-download.ts +36 -0
- package/src/api/types.ts +3 -0
- package/src/astro/integration/routes.ts +31 -0
- package/src/astro/integration/vite-config.ts +26 -0
- package/src/astro/middleware/auth.ts +30 -7
- package/src/astro/middleware/media-usage-write-fence.ts +106 -11
- package/src/astro/middleware/request-context.ts +16 -2
- package/src/astro/middleware.ts +38 -18
- package/src/astro/routes/admin.astro +3 -1
- package/src/astro/routes/api/admin/hooks/exclusive/index.ts +1 -5
- package/src/astro/routes/api/admin/plugins/[id]/disable.ts +4 -3
- package/src/astro/routes/api/admin/plugins/[id]/enable.ts +4 -3
- package/src/astro/routes/api/admin/plugins/[id]/uninstall.ts +4 -3
- package/src/astro/routes/api/admin/plugins/[id]/update.ts +4 -3
- package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +4 -3
- package/src/astro/routes/api/admin/plugins/registry/[id]/uninstall.ts +4 -3
- package/src/astro/routes/api/admin/plugins/registry/[id]/update.ts +3 -3
- package/src/astro/routes/api/admin/plugins/registry/install.ts +3 -3
- package/src/astro/routes/api/admin/plugins/registry/publisher-handle.ts +59 -0
- package/src/astro/routes/api/admin/transfer/[...path].ts +143 -0
- package/src/astro/routes/api/admin/transfer/approvals/[id]/approve.ts +35 -0
- package/src/astro/routes/api/admin/transfer/approvals/[id]/deny.ts +35 -0
- package/src/astro/routes/api/admin/transfer/approvals/index.ts +33 -0
- package/src/astro/routes/api/admin/transfer/capabilities.ts +36 -0
- package/src/astro/routes/api/admin/transfer/exports/[id]/advance.ts +34 -0
- package/src/astro/routes/api/admin/transfer/exports/[id]/archive.ts +35 -0
- package/src/astro/routes/api/admin/transfer/exports/[id]/files/[...path].ts +40 -0
- package/src/astro/routes/api/admin/transfer/exports/[id]/index.ts +25 -0
- package/src/astro/routes/api/admin/transfer/exports/[id]/manifest.ts +38 -0
- package/src/astro/routes/api/admin/transfer/exports/index.ts +64 -0
- package/src/astro/routes/api/admin/transfer/imports/[id]/abandon.ts +28 -0
- package/src/astro/routes/api/admin/transfer/imports/[id]/advance.ts +37 -0
- package/src/astro/routes/api/admin/transfer/imports/[id]/analyze.ts +51 -0
- package/src/astro/routes/api/admin/transfer/imports/[id]/cancel.ts +27 -0
- package/src/astro/routes/api/admin/transfer/imports/[id]/execute.ts +48 -0
- package/src/astro/routes/api/admin/transfer/imports/[id]/files/[...path].ts +54 -0
- package/src/astro/routes/api/admin/transfer/imports/[id]/index.ts +25 -0
- package/src/astro/routes/api/admin/transfer/imports/[id]/missing.ts +34 -0
- package/src/astro/routes/api/admin/transfer/imports/[id]/plan.ts +29 -0
- package/src/astro/routes/api/admin/transfer/imports/[id]/receipt.ts +25 -0
- package/src/astro/routes/api/admin/transfer/imports/index.ts +85 -0
- package/src/astro/routes/api/admin/users/[id]/send-recovery.ts +9 -1
- package/src/astro/routes/api/auth/invite/index.ts +15 -1
- package/src/astro/routes/api/auth/magic-link/send.ts +9 -1
- package/src/astro/routes/api/auth/magic-link/verify.ts +37 -39
- package/src/astro/routes/api/auth/passkey/options.ts +2 -21
- package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +64 -13
- package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +16 -1
- package/src/astro/routes/api/import/wordpress/rewrite-url-helpers.ts +11 -4
- package/src/astro/routes/api/mcp.ts +1 -0
- package/src/astro/routes/api/media/[id].ts +5 -15
- package/src/astro/routes/api/media/file/[...key].ts +30 -5
- package/src/astro/routes/api/oauth/authorize.ts +4 -0
- package/src/astro/routes/api/schema/block-types/[slug]/index.ts +29 -0
- package/src/astro/routes/api/schema/block-types/[slug]/versions/[version]/activate.ts +31 -0
- package/src/astro/routes/api/schema/block-types/index.ts +29 -0
- package/src/astro/routes/api/taxonomies/bulk-tag.ts +29 -0
- package/src/astro/routes/api/typegen.ts +8 -4
- package/src/astro/types.ts +4 -0
- package/src/auth/api-tokens.ts +1 -0
- package/src/auth/rate-limit.ts +2 -1
- package/src/auth/scopes.ts +11 -0
- package/src/cleanup.ts +14 -0
- package/src/cli/client-factory.ts +6 -1
- package/src/cli/commands/export-seed.ts +41 -15
- package/src/cli/commands/seed.ts +6 -2
- package/src/cli/commands/site.ts +556 -0
- package/src/cli/index.ts +2 -0
- package/src/cli/site/archive.ts +273 -0
- package/src/cli/site/export.ts +311 -0
- package/src/cli/site/import.ts +815 -0
- package/src/cli/site/shared.ts +241 -0
- package/src/client/index.ts +414 -11
- package/src/client/portable-text.ts +41 -0
- package/src/comments/public-submission.ts +19 -11
- package/src/components/Blocks.astro +46 -0
- package/src/components/Gallery.astro +2 -1
- package/src/components/Image.astro +51 -6
- package/src/components/InlinePortableTextEditor.tsx +28 -18
- package/src/components/blocks.ts +44 -0
- package/src/components/index.ts +14 -4
- package/src/content/converters/gallery.ts +54 -16
- package/src/content/converters/image-link.ts +27 -0
- package/src/content/converters/index.ts +1 -0
- package/src/content/converters/portable-text-to-prosemirror.ts +10 -7
- package/src/content/converters/prosemirror-to-portable-text.ts +14 -0
- package/src/content/converters/types.ts +17 -0
- package/src/database/content-datetime.ts +19 -2
- package/src/database/migrations/035_bounded_404_log.ts +28 -39
- package/src/database/migrations/081_redirect_write_guards.ts +7 -4
- package/src/database/migrations/083_block_types.ts +58 -0
- package/src/database/migrations/084_site_transfer.ts +264 -0
- package/src/database/migrations/085_taxonomy_def_groups.ts +155 -0
- package/src/database/migrations/runner.ts +6 -0
- package/src/database/repositories/audit.ts +10 -1
- package/src/database/repositories/byline.ts +4 -1
- package/src/database/repositories/content.ts +83 -11
- package/src/database/repositories/media-usage.ts +43 -13
- package/src/database/repositories/media.ts +32 -1
- package/src/database/repositories/options.ts +12 -0
- package/src/database/repositories/revision.ts +6 -3
- package/src/database/repositories/taxonomy-def.ts +126 -0
- package/src/database/repositories/taxonomy.ts +65 -26
- package/src/database/types.ts +133 -1
- package/src/db/sqlite.ts +1 -1
- package/src/emdash-runtime.ts +287 -51
- package/src/import/utils.ts +8 -1
- package/src/import/wxr-taxonomies.ts +11 -67
- package/src/index.ts +26 -1
- package/src/loader.ts +9 -5
- package/src/mcp/server.ts +521 -29
- package/src/mcp/transfer.ts +569 -0
- package/src/media/local-runtime.ts +5 -18
- package/src/media/upload-attempts.ts +5 -0
- package/src/media/url.ts +15 -0
- package/src/media/usage/content-fields.ts +69 -5
- package/src/media/usage/content-refresh.ts +13 -66
- package/src/media/usage/content-repair.ts +10 -2
- package/src/media/usage/extractor.ts +74 -3
- package/src/media/usage/projection-fingerprint.ts +14 -0
- package/src/media/usage/schema-invalidation.ts +72 -0
- package/src/media/usage/site-settings.ts +52 -0
- package/src/media/usage/types.ts +2 -0
- package/src/object-cache/index.ts +69 -4
- package/src/plugin-test-runtime.ts +1 -0
- package/src/plugins/context.ts +104 -56
- package/src/plugins/cron.ts +6 -4
- package/src/plugins/define-plugin.ts +11 -1
- package/src/plugins/email-console.ts +2 -0
- package/src/plugins/email.ts +16 -0
- package/src/plugins/hooks.ts +33 -9
- package/src/plugins/manifest-schema.ts +1 -0
- package/src/plugins/route-error.ts +57 -0
- package/src/plugins/routes.ts +5 -60
- package/src/plugins/sandbox/types.ts +22 -2
- package/src/plugins/types.ts +4 -0
- package/src/registry/artifact-fetch.ts +17 -4
- package/src/registry/publisher-handle.ts +273 -0
- package/src/schema/block-type-contract.ts +478 -0
- package/src/schema/block-type-registry.ts +689 -0
- package/src/schema/block-types.ts +110 -0
- package/src/schema/block-values.ts +293 -0
- package/src/schema/byline-registry.ts +1 -1
- package/src/schema/index.ts +28 -0
- package/src/schema/registry.ts +462 -12
- package/src/schema/types.ts +12 -1
- package/src/schema/zod-generator.ts +92 -9
- package/src/search/fts-manager.ts +40 -0
- package/src/seed/apply.ts +173 -49
- package/src/seed/types.ts +23 -3
- package/src/seed/validate.ts +131 -7
- package/src/settings/index.ts +32 -9
- package/src/settings/types.ts +15 -0
- package/src/taxonomies/index.ts +72 -83
- package/src/transfer/analyze/index.ts +45 -0
- package/src/transfer/analyze/issues.ts +55 -0
- package/src/transfer/analyze/plan.ts +327 -0
- package/src/transfer/analyze/read.ts +134 -0
- package/src/transfer/analyze/step.ts +551 -0
- package/src/transfer/analyze/target.ts +269 -0
- package/src/transfer/analyze/unique-keys.ts +228 -0
- package/src/transfer/analyze/validate.ts +1412 -0
- package/src/transfer/analyze/values.ts +227 -0
- package/src/transfer/analyze/write-rules.ts +196 -0
- package/src/transfer/audit.ts +38 -0
- package/src/transfer/auth.ts +25 -0
- package/src/transfer/container/tar.ts +242 -0
- package/src/transfer/domain.ts +248 -0
- package/src/transfer/errors.ts +134 -0
- package/src/transfer/export/exporter.ts +1067 -0
- package/src/transfer/export/fence.ts +84 -0
- package/src/transfer/export/media.ts +165 -0
- package/src/transfer/export/readers.ts +1164 -0
- package/src/transfer/export/verify.ts +677 -0
- package/src/transfer/fence.ts +193 -0
- package/src/transfer/format/canonical.ts +151 -0
- package/src/transfer/format/column-codec.ts +275 -0
- package/src/transfer/format/columns.ts +707 -0
- package/src/transfer/format/digest.ts +287 -0
- package/src/transfer/format/features.ts +91 -0
- package/src/transfer/format/issues.ts +39 -0
- package/src/transfer/format/kinds.ts +936 -0
- package/src/transfer/format/limits.ts +87 -0
- package/src/transfer/format/manifest.ts +297 -0
- package/src/transfer/format/media-refs.ts +716 -0
- package/src/transfer/format/paths.ts +73 -0
- package/src/transfer/format/plan.ts +162 -0
- package/src/transfer/format/receipt.ts +43 -0
- package/src/transfer/format/records.ts +127 -0
- package/src/transfer/format/settings.ts +94 -0
- package/src/transfer/format/transformations.ts +260 -0
- package/src/transfer/format/version.ts +9 -0
- package/src/transfer/gc.ts +103 -0
- package/src/transfer/import/advance.ts +522 -0
- package/src/transfer/import/context.ts +281 -0
- package/src/transfer/import/entries.ts +186 -0
- package/src/transfer/import/ids.ts +40 -0
- package/src/transfer/import/index.ts +15 -0
- package/src/transfer/import/media.ts +141 -0
- package/src/transfer/import/rebuild.ts +300 -0
- package/src/transfer/import/rows.ts +114 -0
- package/src/transfer/import/scaffold.ts +221 -0
- package/src/transfer/import/sql.ts +134 -0
- package/src/transfer/import/stages.ts +329 -0
- package/src/transfer/import/writers.ts +370 -0
- package/src/transfer/index.ts +472 -0
- package/src/transfer/ops/approvals.ts +359 -0
- package/src/transfer/ops/budget.ts +82 -0
- package/src/transfer/ops/collation.ts +21 -0
- package/src/transfer/ops/identity-map.ts +142 -0
- package/src/transfer/ops/media-blobs.ts +135 -0
- package/src/transfer/ops/operations.ts +842 -0
- package/src/transfer/ops/package-index.ts +159 -0
- package/src/transfer/ops/staged-files.ts +231 -0
- package/src/transfer/ops/states.ts +265 -0
- package/src/transfer/ops/time.ts +54 -0
- package/src/transfer/schema-importer.ts +34 -0
- package/src/transfer/site-id.ts +30 -0
- package/src/transfer/staging/assembler.ts +217 -0
- package/src/transfer/staging/keys.ts +94 -0
- package/src/transfer/staging/package.ts +229 -0
- package/src/transfer/staging/stage.ts +231 -0
- package/src/ui.ts +19 -0
- package/src/visual-editing/toolbar.ts +125 -37
- package/dist/action-token-On1sot_c.mjs +0 -26
- package/dist/action-token-On1sot_c.mjs.map +0 -1
- package/dist/activation-CaMfdBt1.mjs +0 -382
- package/dist/activation-CaMfdBt1.mjs.map +0 -1
- package/dist/adapters-Cq10K4_c.d.mts +0 -171
- package/dist/adapters-Cq10K4_c.d.mts.map +0 -1
- package/dist/allowed-origins-1Sa-2wZK.mjs +0 -116
- package/dist/allowed-origins-1Sa-2wZK.mjs.map +0 -1
- package/dist/api-DmjKxPCY.mjs +0 -4831
- package/dist/api-DmjKxPCY.mjs.map +0 -1
- package/dist/api-tokens-BIXItFKV.mjs +0 -163
- package/dist/api-tokens-BIXItFKV.mjs.map +0 -1
- package/dist/api-tokens-DS1VzN7_.mjs +0 -3
- package/dist/apply-drpfmqrR.mjs +0 -1005
- package/dist/apply-drpfmqrR.mjs.map +0 -1
- package/dist/artifact-fetch-COiMhVgk.mjs +0 -813
- package/dist/artifact-fetch-COiMhVgk.mjs.map +0 -1
- package/dist/authoritative-records-DbqshfKD.mjs +0 -5172
- package/dist/authoritative-records-DbqshfKD.mjs.map +0 -1
- package/dist/authorize-BXpfMpp0.mjs +0 -40
- package/dist/authorize-BXpfMpp0.mjs.map +0 -1
- package/dist/backup-BsPQdwnS.mjs +0 -246
- package/dist/backup-BsPQdwnS.mjs.map +0 -1
- package/dist/byline-Doh-wt99.mjs +0 -906
- package/dist/byline-Doh-wt99.mjs.map +0 -1
- package/dist/byline-fields-CsrdvaLV.mjs +0 -123
- package/dist/byline-fields-CsrdvaLV.mjs.map +0 -1
- package/dist/byline-registry-D6lWzXk6.mjs +0 -406
- package/dist/byline-registry-D6lWzXk6.mjs.map +0 -1
- package/dist/bylines-Cop0hkoy.mjs +0 -214
- package/dist/bylines-Cop0hkoy.mjs.map +0 -1
- package/dist/bylines-kpBzHPr8.mjs +0 -232
- package/dist/bylines-kpBzHPr8.mjs.map +0 -1
- package/dist/cache-B0pOlXdM.mjs +0 -108
- package/dist/cache-B0pOlXdM.mjs.map +0 -1
- package/dist/capture-triggers-gxQxnxrJ.mjs +0 -362
- package/dist/capture-triggers-gxQxnxrJ.mjs.map +0 -1
- package/dist/challenge-store-CkRdSdd8.mjs +0 -51
- package/dist/challenge-store-CkRdSdd8.mjs.map +0 -1
- package/dist/checksum-Bsi2D9M1.mjs +0 -162
- package/dist/checksum-Bsi2D9M1.mjs.map +0 -1
- package/dist/chrome-tags-D7LGB91U.mjs +0 -24
- package/dist/chrome-tags-D7LGB91U.mjs.map +0 -1
- package/dist/chunks-BmWyou79.mjs +0 -25
- package/dist/chunks-BmWyou79.mjs.map +0 -1
- package/dist/collection-deletion-C3Xixmkx.mjs +0 -436
- package/dist/collection-deletion-C3Xixmkx.mjs.map +0 -1
- package/dist/collection-slugs-cache-D9auiKmI.mjs +0 -85
- package/dist/collection-slugs-cache-D9auiKmI.mjs.map +0 -1
- package/dist/comment-DcT8qdPK.mjs +0 -313
- package/dist/comment-DcT8qdPK.mjs.map +0 -1
- package/dist/comment-reaction-DY5-y8Y5.mjs +0 -85
- package/dist/comment-reaction-DY5-y8Y5.mjs.map +0 -1
- package/dist/comments-B67VRNGw.mjs +0 -204
- package/dist/comments-B67VRNGw.mjs.map +0 -1
- package/dist/comments-BVzzt6x5.mjs +0 -99
- package/dist/comments-BVzzt6x5.mjs.map +0 -1
- package/dist/components-DIl9Qzh6.mjs +0 -108
- package/dist/components-DIl9Qzh6.mjs.map +0 -1
- package/dist/config-B2acLeC5.mjs +0 -242
- package/dist/config-B2acLeC5.mjs.map +0 -1
- package/dist/config-CGVeT8Hc.mjs +0 -73
- package/dist/config-CGVeT8Hc.mjs.map +0 -1
- package/dist/config-D3HhLON3.d.mts +0 -39
- package/dist/config-D3HhLON3.d.mts.map +0 -1
- package/dist/config-DS7e4EZ9.d.mts +0 -6387
- package/dist/config-DS7e4EZ9.d.mts.map +0 -1
- package/dist/content-DZ3B5qtA.mjs +0 -2216
- package/dist/content-DZ3B5qtA.mjs.map +0 -1
- package/dist/content-list-query-CbPdzsna.d.mts +0 -26
- package/dist/content-list-query-CbPdzsna.d.mts.map +0 -1
- package/dist/content-policy-CM4skrNi.mjs +0 -46
- package/dist/content-policy-CM4skrNi.mjs.map +0 -1
- package/dist/content-refresh-BHLHJgyt.mjs +0 -3599
- package/dist/content-refresh-BHLHJgyt.mjs.map +0 -1
- package/dist/content-repair-BVll5WNl.mjs +0 -440
- package/dist/content-repair-BVll5WNl.mjs.map +0 -1
- package/dist/context-B195kjqZ.mjs +0 -1835
- package/dist/context-B195kjqZ.mjs.map +0 -1
- package/dist/cron-BcR3RN4K.mjs +0 -282
- package/dist/cron-BcR3RN4K.mjs.map +0 -1
- package/dist/dashboard-CZD4G7k_.mjs +0 -151
- package/dist/dashboard-CZD4G7k_.mjs.map +0 -1
- package/dist/default-BtlxWgh9.mjs +0 -81
- package/dist/default-BtlxWgh9.mjs.map +0 -1
- package/dist/deferred-tasks-Ba9fphlE.d.mts +0 -9
- package/dist/deferred-tasks-Ba9fphlE.d.mts.map +0 -1
- package/dist/device-flow-3E5l0fpH.mjs +0 -466
- package/dist/device-flow-3E5l0fpH.mjs.map +0 -1
- package/dist/dialect-helpers-CtYdsX4W.mjs +0 -307
- package/dist/dialect-helpers-CtYdsX4W.mjs.map +0 -1
- package/dist/direct-executor-ZI8E01Yt.mjs +0 -104
- package/dist/direct-executor-ZI8E01Yt.mjs.map +0 -1
- package/dist/dist-2hDLsLeq.mjs +0 -810
- package/dist/dist-2hDLsLeq.mjs.map +0 -1
- package/dist/email-console-gc-E3Eqj.mjs +0 -50
- package/dist/email-console-gc-E3Eqj.mjs.map +0 -1
- package/dist/emdash-runtime-DZGinqTL.mjs +0 -4041
- package/dist/emdash-runtime-DZGinqTL.mjs.map +0 -1
- package/dist/enrich-C2Mk0lwE.mjs +0 -194
- package/dist/enrich-C2Mk0lwE.mjs.map +0 -1
- package/dist/entry-lock-D3C99MXS.mjs +0 -1520
- package/dist/entry-lock-D3C99MXS.mjs.map +0 -1
- package/dist/entry-lock-D9rnj2Lu.mjs +0 -163
- package/dist/entry-lock-D9rnj2Lu.mjs.map +0 -1
- package/dist/entry-lock-DX7QkqEg.d.mts +0 -3503
- package/dist/entry-lock-DX7QkqEg.d.mts.map +0 -1
- package/dist/entry-locks-CO_KaoU5.mjs +0 -167
- package/dist/entry-locks-CO_KaoU5.mjs.map +0 -1
- package/dist/error-CCTCskie.mjs +0 -90
- package/dist/error-CCTCskie.mjs.map +0 -1
- package/dist/errors-W3PA8HqB.mjs +0 -447
- package/dist/errors-W3PA8HqB.mjs.map +0 -1
- package/dist/escape-DN4Qmjsl.mjs +0 -9
- package/dist/escape-DN4Qmjsl.mjs.map +0 -1
- package/dist/field-defs-cache-B337z8UJ.mjs +0 -77
- package/dist/field-defs-cache-B337z8UJ.mjs.map +0 -1
- package/dist/fts-manager-DMb0sbIQ.mjs +0 -413
- package/dist/fts-manager-DMb0sbIQ.mjs.map +0 -1
- package/dist/hash-CLVFzWvx.mjs +0 -37
- package/dist/hash-CLVFzWvx.mjs.map +0 -1
- package/dist/hook-result-KY1jq21o.d.mts +0 -31
- package/dist/hook-result-KY1jq21o.d.mts.map +0 -1
- package/dist/hreflang-BgS2FqhH.d.mts +0 -50
- package/dist/hreflang-BgS2FqhH.d.mts.map +0 -1
- package/dist/hreflang-CrJwqoeb.mjs +0 -105
- package/dist/hreflang-CrJwqoeb.mjs.map +0 -1
- package/dist/http-route-dispatch-DAKlanXb.mjs +0 -339
- package/dist/http-route-dispatch-DAKlanXb.mjs.map +0 -1
- package/dist/identity-DRGSHEEr.mjs +0 -35
- package/dist/identity-DRGSHEEr.mjs.map +0 -1
- package/dist/import-DFr5B_hC.mjs +0 -2020
- package/dist/import-DFr5B_hC.mjs.map +0 -1
- package/dist/index-CDwKDkTq.d.mts +0 -623
- package/dist/index-CDwKDkTq.d.mts.map +0 -1
- package/dist/install-finalization-5Os-QZ-2.mjs +0 -58
- package/dist/install-finalization-5Os-QZ-2.mjs.map +0 -1
- package/dist/lifecycle-7e-Lt_kX.mjs +0 -25
- package/dist/lifecycle-7e-Lt_kX.mjs.map +0 -1
- package/dist/load-Cu5bvCZh.mjs +0 -28
- package/dist/load-Cu5bvCZh.mjs.map +0 -1
- package/dist/loader-t4TxaFiQ.mjs +0 -1004
- package/dist/loader-t4TxaFiQ.mjs.map +0 -1
- package/dist/manifest-builder-D31UoJCu.mjs +0 -189
- package/dist/manifest-builder-D31UoJCu.mjs.map +0 -1
- package/dist/manifest-schema-Bkh8SeHd.mjs +0 -491
- package/dist/manifest-schema-Bkh8SeHd.mjs.map +0 -1
- package/dist/media-TBlKCwo7.mjs +0 -334
- package/dist/media-TBlKCwo7.mjs.map +0 -1
- package/dist/media-allowlist-s60eAIf6.mjs +0 -44
- package/dist/media-allowlist-s60eAIf6.mjs.map +0 -1
- package/dist/media-folders-ClIRLUQv.mjs +0 -210
- package/dist/media-folders-ClIRLUQv.mjs.map +0 -1
- package/dist/media-url-DY_ijO3B.mjs +0 -43
- package/dist/media-url-DY_ijO3B.mjs.map +0 -1
- package/dist/media-usage-C4NolTpQ.mjs +0 -1833
- package/dist/media-usage-C4NolTpQ.mjs.map +0 -1
- package/dist/media-usage-activation-CAhmb14O.mjs +0 -78
- package/dist/media-usage-activation-CAhmb14O.mjs.map +0 -1
- package/dist/media-usage-work-Cu5DqewW.mjs +0 -586
- package/dist/media-usage-work-Cu5DqewW.mjs.map +0 -1
- package/dist/media-usage-work-DJr3rOEN.mjs +0 -152
- package/dist/media-usage-work-DJr3rOEN.mjs.map +0 -1
- package/dist/media-usage-write-fence-DW5oHT7q.mjs +0 -42
- package/dist/media-usage-write-fence-DW5oHT7q.mjs.map +0 -1
- package/dist/menus-DbgU9z5Q.mjs +0 -3732
- package/dist/menus-DbgU9z5Q.mjs.map +0 -1
- package/dist/menus-Wy8m5NcH.mjs +0 -753
- package/dist/menus-Wy8m5NcH.mjs.map +0 -1
- package/dist/mime-CCYRC4r1.mjs +0 -36
- package/dist/mime-CCYRC4r1.mjs.map +0 -1
- package/dist/mode-CkmOhFqj.mjs +0 -23
- package/dist/mode-CkmOhFqj.mjs.map +0 -1
- package/dist/node-sqlite-compat-vpVaILnb.mjs +0 -46
- package/dist/node-sqlite-compat-vpVaILnb.mjs.map +0 -1
- package/dist/normalize-dChXQjwS.mjs +0 -191
- package/dist/normalize-dChXQjwS.mjs.map +0 -1
- package/dist/oauth-authorization-DZ-R-hmx.mjs +0 -274
- package/dist/oauth-authorization-DZ-R-hmx.mjs.map +0 -1
- package/dist/oauth-clients-BMPA8vjl.mjs +0 -266
- package/dist/oauth-clients-BMPA8vjl.mjs.map +0 -1
- package/dist/oauth-state-store-0gIdb48-.mjs +0 -50
- package/dist/oauth-state-store-0gIdb48-.mjs.map +0 -1
- package/dist/oauth-user-lookup-ChVW4UWu.mjs +0 -26
- package/dist/oauth-user-lookup-ChVW4UWu.mjs.map +0 -1
- package/dist/object-cache-DlSVbn5Y.mjs +0 -534
- package/dist/object-cache-DlSVbn5Y.mjs.map +0 -1
- package/dist/options-D5WcorD5.mjs +0 -200
- package/dist/options-D5WcorD5.mjs.map +0 -1
- package/dist/options-DAknChnw.d.mts +0 -77
- package/dist/options-DAknChnw.d.mts.map +0 -1
- package/dist/parse-Cr379Av9.mjs +0 -81
- package/dist/parse-Cr379Av9.mjs.map +0 -1
- package/dist/parse-tj9XLNO_.d.mts +0 -165
- package/dist/parse-tj9XLNO_.d.mts.map +0 -1
- package/dist/passkey-config-BD-wMaCf.mjs +0 -46
- package/dist/passkey-config-BD-wMaCf.mjs.map +0 -1
- package/dist/patterns-Do3iqi7N.mjs +0 -145
- package/dist/patterns-Do3iqi7N.mjs.map +0 -1
- package/dist/placeholder-CubPFn_L.d.mts +0 -306
- package/dist/placeholder-CubPFn_L.d.mts.map +0 -1
- package/dist/plugins-DHurtdXQ.mjs +0 -277
- package/dist/plugins-DHurtdXQ.mjs.map +0 -1
- package/dist/policy-lyZnjpwz.mjs +0 -45
- package/dist/policy-lyZnjpwz.mjs.map +0 -1
- package/dist/portable-text-oAwodbPS.mjs +0 -286
- package/dist/portable-text-oAwodbPS.mjs.map +0 -1
- package/dist/preview-Cup2bj8c.mjs +0 -107
- package/dist/preview-Cup2bj8c.mjs.map +0 -1
- package/dist/protocol-C1CNiKX4.d.mts +0 -61
- package/dist/protocol-C1CNiKX4.d.mts.map +0 -1
- package/dist/public-submission-Cs-pFMQK.mjs +0 -245
- package/dist/public-submission-Cs-pFMQK.mjs.map +0 -1
- package/dist/public-url-BWVHsfQv.mjs +0 -101
- package/dist/public-url-BWVHsfQv.mjs.map +0 -1
- package/dist/query-D-lxiOgn.mjs +0 -890
- package/dist/query-D-lxiOgn.mjs.map +0 -1
- package/dist/query-D7kHKOws.mjs +0 -41
- package/dist/query-D7kHKOws.mjs.map +0 -1
- package/dist/rate-limit-CqJn-w4h.mjs +0 -120
- package/dist/rate-limit-CqJn-w4h.mjs.map +0 -1
- package/dist/redirect-BrChFPIu.mjs +0 -17
- package/dist/redirect-BrChFPIu.mjs.map +0 -1
- package/dist/redirect-D2Yo-b6S.mjs +0 -415
- package/dist/redirect-D2Yo-b6S.mjs.map +0 -1
- package/dist/redirects-BCiTaIQ-.mjs +0 -146
- package/dist/redirects-BCiTaIQ-.mjs.map +0 -1
- package/dist/redirects-ByItliuu.mjs +0 -644
- package/dist/redirects-ByItliuu.mjs.map +0 -1
- package/dist/registry-CkuPoZs7.mjs +0 -1222
- package/dist/registry-CkuPoZs7.mjs.map +0 -1
- package/dist/relation-XTAtOpJO.mjs +0 -334
- package/dist/relation-XTAtOpJO.mjs.map +0 -1
- package/dist/relations-B4w-G5U9.mjs +0 -309
- package/dist/relations-B4w-G5U9.mjs.map +0 -1
- package/dist/relations-O63JWBRj.mjs +0 -403
- package/dist/relations-O63JWBRj.mjs.map +0 -1
- package/dist/request-cache-DaMhSQdx.mjs +0 -104
- package/dist/request-cache-DaMhSQdx.mjs.map +0 -1
- package/dist/request-meta-C8yl_sIs.mjs +0 -141
- package/dist/request-meta-C8yl_sIs.mjs.map +0 -1
- package/dist/resolve-BJIS8wKr.mjs +0 -199
- package/dist/resolve-BJIS8wKr.mjs.map +0 -1
- package/dist/route-wire-D66L2oaZ.mjs +0 -197
- package/dist/route-wire-D66L2oaZ.mjs.map +0 -1
- package/dist/routes-DYsTe-oR.mjs +0 -321
- package/dist/routes-DYsTe-oR.mjs.map +0 -1
- package/dist/runner-3gfxRsJG.d.mts +0 -68
- package/dist/runner-3gfxRsJG.d.mts.map +0 -1
- package/dist/runner-ClrleR8A.mjs +0 -5502
- package/dist/runner-ClrleR8A.mjs.map +0 -1
- package/dist/schema-CPhPM9Do.mjs +0 -420
- package/dist/schema-CPhPM9Do.mjs.map +0 -1
- package/dist/schema-DEERRrKS.mjs +0 -9
- package/dist/schema-DEERRrKS.mjs.map +0 -1
- package/dist/scopes-Bn8_rCSt.mjs +0 -27
- package/dist/scopes-Bn8_rCSt.mjs.map +0 -1
- package/dist/search-lami_Ota.mjs +0 -448
- package/dist/search-lami_Ota.mjs.map +0 -1
- package/dist/secrets-DNgVgz_e.mjs +0 -341
- package/dist/secrets-DNgVgz_e.mjs.map +0 -1
- package/dist/sections-BIpd4Cxf.mjs +0 -346
- package/dist/sections-BIpd4Cxf.mjs.map +0 -1
- package/dist/seo-BtU70b7l.mjs +0 -114
- package/dist/seo-BtU70b7l.mjs.map +0 -1
- package/dist/seo-_4qmazxz.mjs +0 -133
- package/dist/seo-_4qmazxz.mjs.map +0 -1
- package/dist/service-Bpy5iTX0.mjs +0 -150
- package/dist/service-Bpy5iTX0.mjs.map +0 -1
- package/dist/session-user-BQeS2Mrb.mjs +0 -51
- package/dist/session-user-BQeS2Mrb.mjs.map +0 -1
- package/dist/settings-DmroR53o.mjs +0 -420
- package/dist/settings-DmroR53o.mjs.map +0 -1
- package/dist/settings-g2F2a3Ub.mjs +0 -51
- package/dist/settings-g2F2a3Ub.mjs.map +0 -1
- package/dist/setup-complete-BrZaXpVV.mjs +0 -26
- package/dist/setup-complete-BrZaXpVV.mjs.map +0 -1
- package/dist/setup-nonce-Cb9-884O.mjs +0 -25
- package/dist/setup-nonce-Cb9-884O.mjs.map +0 -1
- package/dist/site-url-DjhYCaep.mjs +0 -16
- package/dist/site-url-DjhYCaep.mjs.map +0 -1
- package/dist/snapshot-C32Xuv3o.mjs +0 -233
- package/dist/snapshot-C32Xuv3o.mjs.map +0 -1
- package/dist/ssrf-Co1GGFZz.mjs +0 -406
- package/dist/ssrf-Co1GGFZz.mjs.map +0 -1
- package/dist/state-C7gCoiHr.mjs +0 -153
- package/dist/state-C7gCoiHr.mjs.map +0 -1
- package/dist/status-DLoBmcDz.mjs +0 -30
- package/dist/status-DLoBmcDz.mjs.map +0 -1
- package/dist/storage-query-CPctdMKX.d.mts +0 -1519
- package/dist/storage-query-CPctdMKX.d.mts.map +0 -1
- package/dist/target-CUKlGrXb.mjs +0 -27
- package/dist/target-CUKlGrXb.mjs.map +0 -1
- package/dist/taxonomies-BpqpXJUE.mjs +0 -707
- package/dist/taxonomies-BpqpXJUE.mjs.map +0 -1
- package/dist/taxonomies-ClCo4IGO.mjs +0 -899
- package/dist/taxonomies-ClCo4IGO.mjs.map +0 -1
- package/dist/taxonomy-Dj1vpw5E.mjs +0 -508
- package/dist/taxonomy-Dj1vpw5E.mjs.map +0 -1
- package/dist/tokens-Ns02GAma.mjs +0 -171
- package/dist/tokens-Ns02GAma.mjs.map +0 -1
- package/dist/transport-B8P1xsqk.mjs +0 -136
- package/dist/transport-B8P1xsqk.mjs.map +0 -1
- package/dist/transport-BLHZZcD_.d.mts +0 -42
- package/dist/transport-BLHZZcD_.d.mts.map +0 -1
- package/dist/trusted-proxy-Cg7SuD15.mjs +0 -51
- package/dist/trusted-proxy-Cg7SuD15.mjs.map +0 -1
- package/dist/turnstile-DWCSh_Qc.mjs +0 -53
- package/dist/turnstile-DWCSh_Qc.mjs.map +0 -1
- package/dist/types-B3RkqvDW.mjs +0 -156
- package/dist/types-B3RkqvDW.mjs.map +0 -1
- package/dist/types-BtuppOTV.mjs +0 -16
- package/dist/types-BtuppOTV.mjs.map +0 -1
- package/dist/types-CCR3ivXE.d.mts +0 -363
- package/dist/types-CCR3ivXE.d.mts.map +0 -1
- package/dist/types-CSHgU_-_.mjs +0 -18
- package/dist/types-CSHgU_-_.mjs.map +0 -1
- package/dist/types-CbwqGOXm.d.mts +0 -766
- package/dist/types-CbwqGOXm.d.mts.map +0 -1
- package/dist/types-D4RGIc2I.d.mts +0 -198
- package/dist/types-D4RGIc2I.d.mts.map +0 -1
- package/dist/types-DEHidMEk.d.mts +0 -1637
- package/dist/types-DEHidMEk.d.mts.map +0 -1
- package/dist/types-DQeU2SR7.d.mts +0 -279
- package/dist/types-DQeU2SR7.d.mts.map +0 -1
- package/dist/types-DxQWoQLA.d.mts +0 -264
- package/dist/types-DxQWoQLA.d.mts.map +0 -1
- package/dist/types-FBBnGZ2Y.d.mts +0 -198
- package/dist/types-FBBnGZ2Y.d.mts.map +0 -1
- package/dist/types-ZCnlropY.mjs +0 -69
- package/dist/types-ZCnlropY.mjs.map +0 -1
- package/dist/types-_Qtyawiy.d.mts +0 -124
- package/dist/types-_Qtyawiy.d.mts.map +0 -1
- package/dist/types-gC6FAoa_.d.mts +0 -75
- package/dist/types-gC6FAoa_.d.mts.map +0 -1
- package/dist/types-k--LneIF.mjs +0 -22
- package/dist/types-k--LneIF.mjs.map +0 -1
- package/dist/upload-attempts-2xdnoKGe.mjs +0 -27
- package/dist/upload-attempts-2xdnoKGe.mjs.map +0 -1
- package/dist/url-pattern-3m3reMzr.mjs +0 -33
- package/dist/url-pattern-3m3reMzr.mjs.map +0 -1
- package/dist/user-DroRntkx.mjs +0 -171
- package/dist/user-DroRntkx.mjs.map +0 -1
- package/dist/utils-CC2oNGV8.mjs +0 -435
- package/dist/utils-CC2oNGV8.mjs.map +0 -1
- package/dist/validate-BCP9HNDn.mjs +0 -384
- package/dist/validate-BCP9HNDn.mjs.map +0 -1
- package/dist/validate-CyQ7I3xF.d.mts +0 -410
- package/dist/validate-CyQ7I3xF.d.mts.map +0 -1
- package/dist/validation-DBy6WF94.mjs +0 -216
- package/dist/validation-DBy6WF94.mjs.map +0 -1
- package/dist/version-B57E4Uo7.mjs +0 -7
- package/dist/version-B57E4Uo7.mjs.map +0 -1
- package/dist/widgets-wQ7Xcgq8.mjs +0 -126
- package/dist/widgets-wQ7Xcgq8.mjs.map +0 -1
- package/dist/wxr-taxonomies-DIHfvEDt.mjs +0 -406
- package/dist/wxr-taxonomies-DIHfvEDt.mjs.map +0 -1
- package/dist/wxr-taxonomies-DU2ZSiGc.d.mts +0 -34
- package/dist/wxr-taxonomies-DU2ZSiGc.d.mts.map +0 -1
- package/dist/zod-generator-D_yHIWfs.mjs +0 -329
- package/dist/zod-generator-D_yHIWfs.mjs.map +0 -1
- /package/dist/{ssrf-BRpTDMV6.mjs → ssrf-DpBH7aOt.mjs} +0 -0
|
@@ -0,0 +1,4718 @@
|
|
|
1
|
+
import { n as isMissingTableError, t as isMissingColumnError } from "./db-errors-CBD5yMro.mjs";
|
|
2
|
+
import { i as validateStorageCollectionName, n as validateJsonFieldName, r as validatePluginIdentifier, t as validateIdentifier } from "./validate-DANX1-Qi.mjs";
|
|
3
|
+
import { d as isSqlite, f as jsonExtractExpr, u as isPostgres } from "./dialect-helpers-7_IqPdh9.mjs";
|
|
4
|
+
import { i as isI18nEnabled, o as resolveConfiguredLocale, r as getI18nConfig } from "./config-CvmjJcVw.mjs";
|
|
5
|
+
import { t as after } from "./after-PcEvMY_6.mjs";
|
|
6
|
+
import { t as OptionsRepository } from "./options-DhrI7eMv.mjs";
|
|
7
|
+
import { i as encodeBase64, t as decodeBase64 } from "./base64-DvnNgP7e.mjs";
|
|
8
|
+
import { t as EmDashSecretsError } from "./secrets-CIi0nFiR.mjs";
|
|
9
|
+
import { i as RevisionRepository, r as isSystemOrderField, t as ContentRepository } from "./content-DFaIEDj9.mjs";
|
|
10
|
+
import { o as MAX_COLLECTION_LIST_COLUMNS } from "./types-BcrBrE6-.mjs";
|
|
11
|
+
import { n as chunks, t as SQL_BATCH_SIZE } from "./chunks-Dms7Uxbt.mjs";
|
|
12
|
+
import { a as ScheduledNotDueError, i as InvalidCursorError, n as ContentMutationConflictError, r as EmDashValidationError, t as ContentCollectionNotFoundError } from "./types-BkUg2yHx.mjs";
|
|
13
|
+
import { t as withTransaction } from "./transaction-BcJO4Tmc.mjs";
|
|
14
|
+
import { t as UserRepository } from "./user-DDSL-sGw.mjs";
|
|
15
|
+
import { t as TaxonomyRepository } from "./taxonomy-C3Fu1YeH.mjs";
|
|
16
|
+
import { t as CommentRepository } from "./comment-CrBl22OF.mjs";
|
|
17
|
+
import { t as RedirectRepository } from "./redirect-iJf5fltD.mjs";
|
|
18
|
+
import { r as requestCached } from "./request-cache-DzLYKm_x.mjs";
|
|
19
|
+
import { t as BylineRepository } from "./byline-DYmkXqvo.mjs";
|
|
20
|
+
import { r as parseAllowedMimeTypes, t as matchesMimeAllowlist } from "./mime-ChLqYwDk.mjs";
|
|
21
|
+
import { t as EntryLockRepository } from "./entry-locks-D0foLdjQ.mjs";
|
|
22
|
+
import { t as SeoRepository } from "./seo-D5w3-Azy.mjs";
|
|
23
|
+
import { i as scheduledPolicyRejectionKey } from "./content-policy-DFNw91-d.mjs";
|
|
24
|
+
import { n as invalidateRedirectCache } from "./cache-m7UEkRFV.mjs";
|
|
25
|
+
import { t as FTSManager } from "./fts-manager-S-K0laoS.mjs";
|
|
26
|
+
import { d as invalidateTermCache } from "./taxonomies-C1IwNe8Q.mjs";
|
|
27
|
+
import { a as SchemaRegistry, t as BlockTypeRegistry } from "./block-type-registry-DUI_3KxT.mjs";
|
|
28
|
+
import { r as hashString } from "./hash-SDAW6zC-.mjs";
|
|
29
|
+
import { n as expandCollectionBlockFields } from "./block-values-C_VIPr5t.mjs";
|
|
30
|
+
import { t as ErrorCode } from "./errors-BF0_LFhW.mjs";
|
|
31
|
+
import { t as PluginStateRepository } from "./state-DDiPySSG.mjs";
|
|
32
|
+
import { f as encodePluginSettingValue, l as PluginSettingEncryptionError } from "./settings-CgjRWGW9.mjs";
|
|
33
|
+
import { i as normalizeManifestRoute$1, o as pluginManifestSchema$1, s as reconcileManifestAccess$1 } from "./manifest-schema-CY3zng_M.mjs";
|
|
34
|
+
import { o as withUnavailableReason } from "./types-D7HUsguf.mjs";
|
|
35
|
+
import { a as normalizeCapabilities } from "./types-BZakh7Ym.mjs";
|
|
36
|
+
import { t as EmDashStorageError } from "./types-VT4-BugW.mjs";
|
|
37
|
+
import { i as verificationError, r as verifyMultihash, t as computeArtifactDigestCandidates } from "./checksum-B2IINIlN.mjs";
|
|
38
|
+
import { n as fetchReleaseArtifact, t as fetchRegistryArtifactUrl } from "./artifact-fetch-DZMOGZ3-.mjs";
|
|
39
|
+
import { i as resolveAndValidateExternalUrlTarget } from "./ssrf-CJUnJymJ.mjs";
|
|
40
|
+
import { r as verifyAuthoritativePackageRelease, t as readAuthoritativePackageRelease } from "./authoritative-records-CoJvBXAD.mjs";
|
|
41
|
+
import { a as parseDurationSeconds, l as validateAggregatorUrl, n as coerceRegistryConfig, o as releaseExemptFromMinimumAge, t as canonicalCapabilitiesForDriftCheck } from "./config-IhybvkIQ.mjs";
|
|
42
|
+
import { sql } from "kysely";
|
|
43
|
+
import { canonicalizeDeclaredAccess, pluginManifestSchema, reconcileManifestAccess } from "@emdash-cms/plugin-types";
|
|
44
|
+
import { createGzipDecoder, unpackTar } from "modern-tar";
|
|
45
|
+
import { ClientResponseError, ClientValidationError } from "@atcute/client";
|
|
46
|
+
import { checkEnvCompatibility, findSkippedEnvConstraints } from "@emdash-cms/registry-client/env";
|
|
47
|
+
import { isProvenFirstRelease } from "@emdash-cms/registry-client/listing-policy";
|
|
48
|
+
import { evaluateRegistryReleaseWithdrawal } from "@emdash-cms/registry-client/withdrawal";
|
|
49
|
+
import { NSID } from "@emdash-cms/registry-lexicons";
|
|
50
|
+
|
|
51
|
+
//#region src/api/rev.ts
|
|
52
|
+
/**
|
|
53
|
+
* Generate a _rev token from a content item's version and updatedAt.
|
|
54
|
+
*/
|
|
55
|
+
function encodeRev(item) {
|
|
56
|
+
return encodeBase64(`${item.version}:${item.updatedAt}`);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Decode a _rev token into its components.
|
|
60
|
+
* Returns null if the token is malformed.
|
|
61
|
+
*/
|
|
62
|
+
function decodeRev(rev) {
|
|
63
|
+
try {
|
|
64
|
+
const decoded = decodeBase64(rev);
|
|
65
|
+
const colonIdx = decoded.indexOf(":");
|
|
66
|
+
if (colonIdx === -1) return null;
|
|
67
|
+
const version = parseInt(decoded.slice(0, colonIdx), 10);
|
|
68
|
+
const updatedAt = decoded.slice(colonIdx + 1);
|
|
69
|
+
if (isNaN(version) || !updatedAt) return null;
|
|
70
|
+
return {
|
|
71
|
+
version,
|
|
72
|
+
updatedAt
|
|
73
|
+
};
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Validate a _rev token against a content item.
|
|
80
|
+
* Returns null if valid (or if no _rev provided), or an error message if invalid.
|
|
81
|
+
*/
|
|
82
|
+
function validateRev(rev, item) {
|
|
83
|
+
if (!rev) return { valid: true };
|
|
84
|
+
const decoded = decodeRev(rev);
|
|
85
|
+
if (!decoded) return {
|
|
86
|
+
valid: false,
|
|
87
|
+
message: "Malformed _rev token"
|
|
88
|
+
};
|
|
89
|
+
if (decoded.version !== item.version || decoded.updatedAt !== item.updatedAt) return {
|
|
90
|
+
valid: false,
|
|
91
|
+
message: "Content has been modified since last read (version conflict)"
|
|
92
|
+
};
|
|
93
|
+
return { valid: true };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region src/api/handlers/validate-media-fields.ts
|
|
98
|
+
function asMediaRef(value) {
|
|
99
|
+
if (value === null || value === void 0) return null;
|
|
100
|
+
if (typeof value !== "object" || Array.isArray(value)) return null;
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
function fail(message) {
|
|
104
|
+
return {
|
|
105
|
+
success: false,
|
|
106
|
+
error: {
|
|
107
|
+
code: "INVALID_MIME_FOR_FIELD",
|
|
108
|
+
message
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function addTarget(targets, path, value, allowedMimeTypes, includeDarkVariant) {
|
|
113
|
+
if (!allowedMimeTypes?.length || value == null) return;
|
|
114
|
+
targets.push({
|
|
115
|
+
path,
|
|
116
|
+
value,
|
|
117
|
+
allowedMimeTypes: [...allowedMimeTypes]
|
|
118
|
+
});
|
|
119
|
+
const ref = includeDarkVariant ? asMediaRef(value) : null;
|
|
120
|
+
if (ref && "darkVariant" in ref && ref.darkVariant != null) targets.push({
|
|
121
|
+
path: `${path}.darkVariant`,
|
|
122
|
+
value: ref.darkVariant,
|
|
123
|
+
allowedMimeTypes: [...allowedMimeTypes]
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function collectBlockTargets(targets, field, value) {
|
|
127
|
+
if (!Array.isArray(value)) return {
|
|
128
|
+
success: true,
|
|
129
|
+
data: true
|
|
130
|
+
};
|
|
131
|
+
const types = new Map((field.blockTypes ?? []).map((type) => [type.slug, type]));
|
|
132
|
+
for (const [index, block] of value.entries()) {
|
|
133
|
+
if (!block || typeof block !== "object" || Array.isArray(block)) continue;
|
|
134
|
+
const typeSlug = "_type" in block && typeof block._type === "string" ? block._type : void 0;
|
|
135
|
+
const versionNumber = "_version" in block && typeof block._version === "number" ? block._version : void 0;
|
|
136
|
+
const key = "_key" in block && typeof block._key === "string" ? block._key : String(index);
|
|
137
|
+
const type = typeSlug ? types.get(typeSlug) : void 0;
|
|
138
|
+
const version = type?.versions.find((candidate) => candidate.version === versionNumber);
|
|
139
|
+
if (!type || !version || version.unsupportedTypes?.length) return {
|
|
140
|
+
success: false,
|
|
141
|
+
error: {
|
|
142
|
+
code: "UNSUPPORTED_FIELD_TYPE",
|
|
143
|
+
message: `Field '${field.slug}' block '${key}' has no retained definition`
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
for (const nestedField of version.fields) {
|
|
147
|
+
const nestedValue = nestedField.slug in block ? block[nestedField.slug] : void 0;
|
|
148
|
+
const nestedPath = `${field.slug}.${key}.${nestedField.slug}`;
|
|
149
|
+
if (nestedField.type !== "image" && nestedField.type !== "file") continue;
|
|
150
|
+
addTarget(targets, nestedPath, nestedValue, nestedField.validation?.allowedMimeTypes, nestedField.type === "image");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
success: true,
|
|
155
|
+
data: true
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
async function loadMediaFieldsForCollection(db, collectionSlug) {
|
|
159
|
+
const rows = await db.selectFrom("_emdash_fields").innerJoin("_emdash_collections", "_emdash_collections.id", "_emdash_fields.collection_id").select([
|
|
160
|
+
"_emdash_fields.slug",
|
|
161
|
+
"_emdash_fields.type",
|
|
162
|
+
"_emdash_fields.validation"
|
|
163
|
+
]).where("_emdash_collections.slug", "=", collectionSlug).where("_emdash_fields.type", "in", [
|
|
164
|
+
"file",
|
|
165
|
+
"image",
|
|
166
|
+
"blocks"
|
|
167
|
+
]).execute();
|
|
168
|
+
const out = [];
|
|
169
|
+
const allBlockTypes = rows.some((row) => row.type === "blocks") ? await new BlockTypeRegistry(db).listBlockTypes() : [];
|
|
170
|
+
const blockTypeBySlug = new Map(allBlockTypes.map((type) => [type.slug, type]));
|
|
171
|
+
for (const row of rows) {
|
|
172
|
+
if (row.type === "blocks") {
|
|
173
|
+
let validation;
|
|
174
|
+
try {
|
|
175
|
+
validation = row.validation ? JSON.parse(row.validation) : {};
|
|
176
|
+
} catch {
|
|
177
|
+
validation = {};
|
|
178
|
+
}
|
|
179
|
+
let configured = [];
|
|
180
|
+
if (validation && typeof validation === "object" && !Array.isArray(validation)) {
|
|
181
|
+
const allowed = "allowedTypes" in validation && Array.isArray(validation.allowedTypes) ? validation.allowedTypes : [];
|
|
182
|
+
const retired = "retiredTypes" in validation && Array.isArray(validation.retiredTypes) ? validation.retiredTypes : [];
|
|
183
|
+
configured = [...allowed, ...retired];
|
|
184
|
+
}
|
|
185
|
+
out.push({
|
|
186
|
+
slug: row.slug,
|
|
187
|
+
type: row.type,
|
|
188
|
+
allowedMimeTypes: [],
|
|
189
|
+
blockTypes: configured.flatMap((slug) => {
|
|
190
|
+
const type = typeof slug === "string" ? blockTypeBySlug.get(slug) : void 0;
|
|
191
|
+
return type ? [type] : [];
|
|
192
|
+
})
|
|
193
|
+
});
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
const list = parseAllowedMimeTypes(row.validation);
|
|
197
|
+
if (!list) continue;
|
|
198
|
+
out.push({
|
|
199
|
+
slug: row.slug,
|
|
200
|
+
type: row.type,
|
|
201
|
+
allowedMimeTypes: list
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
async function validateMediaFields(db, collectionSlug, data) {
|
|
207
|
+
const fields = await requestCached(`mediaFields:${collectionSlug}`, () => loadMediaFieldsForCollection(db, collectionSlug));
|
|
208
|
+
if (fields.length === 0) return {
|
|
209
|
+
success: true,
|
|
210
|
+
data: true
|
|
211
|
+
};
|
|
212
|
+
const targets = [];
|
|
213
|
+
for (const field of fields) {
|
|
214
|
+
const value = data[field.slug];
|
|
215
|
+
if (field.type === "blocks") {
|
|
216
|
+
const collected = collectBlockTargets(targets, field, value);
|
|
217
|
+
if (!collected.success) return collected;
|
|
218
|
+
} else addTarget(targets, field.slug, value, field.allowedMimeTypes, field.type === "image");
|
|
219
|
+
}
|
|
220
|
+
if (targets.length === 0) return {
|
|
221
|
+
success: true,
|
|
222
|
+
data: true
|
|
223
|
+
};
|
|
224
|
+
const localIds = /* @__PURE__ */ new Set();
|
|
225
|
+
for (const target of targets) {
|
|
226
|
+
const ref = asMediaRef(target.value);
|
|
227
|
+
if (!ref) continue;
|
|
228
|
+
if ((typeof ref.provider === "string" ? ref.provider : "local") === "local" && typeof ref.id === "string") localIds.add(ref.id);
|
|
229
|
+
}
|
|
230
|
+
const idList = [...localIds];
|
|
231
|
+
const mimeById = /* @__PURE__ */ new Map();
|
|
232
|
+
if (idList.length > 0) for (const batch of chunks(idList, SQL_BATCH_SIZE)) {
|
|
233
|
+
const rows = await db.selectFrom("media").select(["id", "mime_type"]).where("id", "in", batch).execute();
|
|
234
|
+
for (const r of rows) mimeById.set(r.id, r.mime_type);
|
|
235
|
+
}
|
|
236
|
+
for (const target of targets) {
|
|
237
|
+
const ref = asMediaRef(target.value);
|
|
238
|
+
if (!ref) continue;
|
|
239
|
+
const provider = typeof ref.provider === "string" ? ref.provider : "local";
|
|
240
|
+
let mime;
|
|
241
|
+
if (provider === "local") {
|
|
242
|
+
if (typeof ref.id !== "string") return fail(`Field '${target.path}' references media with an invalid id`);
|
|
243
|
+
mime = mimeById.get(ref.id);
|
|
244
|
+
if (!mime) return fail(`Field '${target.path}' references media with unknown MIME type`);
|
|
245
|
+
} else {
|
|
246
|
+
if (typeof ref.mimeType !== "string") return fail(`Field '${target.path}' requires a mimeType declaration for non-local media`);
|
|
247
|
+
mime = ref.mimeType;
|
|
248
|
+
}
|
|
249
|
+
if (!matchesMimeAllowlist(mime, target.allowedMimeTypes)) return fail(`Field '${target.path}' does not accept ${mime}`);
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
success: true,
|
|
253
|
+
data: true
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
//#endregion
|
|
258
|
+
//#region src/api/handlers/content.ts
|
|
259
|
+
/**
|
|
260
|
+
* Narrow a caught error to one carrying a structured `apiError` discriminant.
|
|
261
|
+
* Used by transaction callbacks that want to surface a specific error code
|
|
262
|
+
* through the standard Error throwing path.
|
|
263
|
+
*/
|
|
264
|
+
function hasApiError(error) {
|
|
265
|
+
if (!(error instanceof Error) || !("apiError" in error)) return false;
|
|
266
|
+
const { apiError } = error;
|
|
267
|
+
return typeof apiError === "object" && apiError !== null && "code" in apiError && typeof apiError.code === "string";
|
|
268
|
+
}
|
|
269
|
+
function isTranslationLocaleConflict(error, collection) {
|
|
270
|
+
if (!(error instanceof Error)) return false;
|
|
271
|
+
const message = error.message.toLowerCase();
|
|
272
|
+
const storedIndexName = `uidx_ec_${collection}_active_tg_locale`.slice(0, 63).toLowerCase();
|
|
273
|
+
return message.includes(storedIndexName) || message.includes("unique constraint failed") && message.includes("translation_group") && message.includes("locale");
|
|
274
|
+
}
|
|
275
|
+
function decodeRevisionPrecondition(rev) {
|
|
276
|
+
if (rev === void 0) return void 0;
|
|
277
|
+
const decoded = decodeRev(rev);
|
|
278
|
+
if (!decoded) throw new ContentMutationConflictError("Revision precondition did not match");
|
|
279
|
+
return decoded;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Extract a slug source (title or name) from content data.
|
|
283
|
+
* Returns null if no suitable string field is found.
|
|
284
|
+
*/
|
|
285
|
+
function getSlugSource(data) {
|
|
286
|
+
if (typeof data.title === "string" && data.title.length > 0) return data.title;
|
|
287
|
+
if (typeof data.name === "string" && data.name.length > 0) return data.name;
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
/** Default SEO values for content without an explicit SEO row */
|
|
291
|
+
const SEO_DEFAULTS = {
|
|
292
|
+
title: null,
|
|
293
|
+
description: null,
|
|
294
|
+
image: null,
|
|
295
|
+
canonical: null,
|
|
296
|
+
noIndex: false
|
|
297
|
+
};
|
|
298
|
+
/**
|
|
299
|
+
* Check if a collection has SEO enabled.
|
|
300
|
+
*/
|
|
301
|
+
async function collectionHasSeo(db, collection) {
|
|
302
|
+
return (await db.selectFrom("_emdash_collections").select("has_seo").where("slug", "=", collection).executeTakeFirst())?.has_seo === 1;
|
|
303
|
+
}
|
|
304
|
+
async function getCollectionPublishConfig(db, collection) {
|
|
305
|
+
const row = await db.selectFrom("_emdash_collections").select(["supports", "routable"]).where("slug", "=", collection).executeTakeFirst();
|
|
306
|
+
const supports = row?.supports ? JSON.parse(row.supports) : [];
|
|
307
|
+
return {
|
|
308
|
+
supportsRevisions: Array.isArray(supports) && supports.includes("revisions"),
|
|
309
|
+
routable: row?.routable !== 0
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function requireRoutablePublishSlug(routable, slug) {
|
|
313
|
+
if (routable && !slug?.trim()) throw new EmDashValidationError("Cannot publish routable content without a slug");
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Hydrate SEO data on a single content item if the collection has SEO enabled.
|
|
317
|
+
*/
|
|
318
|
+
async function hydrateSeo(db, collection, item, hasSeo) {
|
|
319
|
+
if (!hasSeo) return;
|
|
320
|
+
item.seo = await new SeoRepository(db).get(collection, item.id);
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Hydrate SEO data on multiple content items using a single batch query.
|
|
324
|
+
*/
|
|
325
|
+
async function hydrateSeoMany(db, collection, items, hasSeo) {
|
|
326
|
+
if (!hasSeo || items.length === 0) return;
|
|
327
|
+
const seoMap = await new SeoRepository(db).getMany(collection, items.map((i) => i.id));
|
|
328
|
+
for (const item of items) item.seo = seoMap.get(item.id) ?? { ...SEO_DEFAULTS };
|
|
329
|
+
}
|
|
330
|
+
async function hydrateBylines(db, collection, item) {
|
|
331
|
+
const bylineRepo = new BylineRepository(db);
|
|
332
|
+
const localeOpt = item.locale ? { locale: item.locale } : void 0;
|
|
333
|
+
const bylines = await bylineRepo.getContentBylines(collection, item.id, localeOpt);
|
|
334
|
+
if (bylines.length > 0) {
|
|
335
|
+
item.bylines = bylines.map((c) => ({
|
|
336
|
+
...c,
|
|
337
|
+
source: "explicit"
|
|
338
|
+
}));
|
|
339
|
+
item.byline = bylines[0]?.byline ?? null;
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (item.primaryBylineId) {
|
|
343
|
+
item.bylines = [];
|
|
344
|
+
item.byline = null;
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (item.authorId) {
|
|
348
|
+
const fallback = await bylineRepo.findByUserId(item.authorId, localeOpt);
|
|
349
|
+
if (fallback) {
|
|
350
|
+
item.bylines = [{
|
|
351
|
+
byline: fallback,
|
|
352
|
+
sortOrder: 0,
|
|
353
|
+
roleLabel: null,
|
|
354
|
+
source: "inferred"
|
|
355
|
+
}];
|
|
356
|
+
item.byline = fallback;
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
item.bylines = [];
|
|
361
|
+
item.byline = null;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Batch-hydrate bylines for multiple items using two bulk queries instead of N+1.
|
|
365
|
+
*
|
|
366
|
+
* Items may live at different locales (e.g. a list endpoint returning the
|
|
367
|
+
* translations of an entry). Group by `item.locale` and call the strict
|
|
368
|
+
* per-locale repo method once per group so each item resolves against its
|
|
369
|
+
* own locale's byline rows.
|
|
370
|
+
*/
|
|
371
|
+
async function hydrateBylinesMany(db, collection, items) {
|
|
372
|
+
if (items.length === 0) return;
|
|
373
|
+
const bylineRepo = new BylineRepository(db);
|
|
374
|
+
const localeBuckets = /* @__PURE__ */ new Map();
|
|
375
|
+
for (const item of items) {
|
|
376
|
+
const key = item.locale ?? null;
|
|
377
|
+
const bucket = localeBuckets.get(key);
|
|
378
|
+
if (bucket) bucket.push(item);
|
|
379
|
+
else localeBuckets.set(key, [item]);
|
|
380
|
+
}
|
|
381
|
+
const bylinesByItem = /* @__PURE__ */ new Map();
|
|
382
|
+
const itemsNeedingAuthorCheck = [];
|
|
383
|
+
for (const [locale, bucket] of localeBuckets) {
|
|
384
|
+
const localeOpt = locale ? { locale } : void 0;
|
|
385
|
+
const ids = bucket.map((i) => i.id);
|
|
386
|
+
const credits = await bylineRepo.getContentBylinesMany(collection, ids, localeOpt);
|
|
387
|
+
for (const [id, list] of credits) bylinesByItem.set(id, list);
|
|
388
|
+
for (const item of bucket) {
|
|
389
|
+
if (credits.has(item.id) && credits.get(item.id).length > 0) continue;
|
|
390
|
+
if (item.authorId) itemsNeedingAuthorCheck.push(item);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
const fallbackByItem = /* @__PURE__ */ new Map();
|
|
394
|
+
if (itemsNeedingAuthorCheck.length > 0) {
|
|
395
|
+
const authorBuckets = /* @__PURE__ */ new Map();
|
|
396
|
+
for (const item of itemsNeedingAuthorCheck) {
|
|
397
|
+
if (item.primaryBylineId) continue;
|
|
398
|
+
const key = item.locale ?? null;
|
|
399
|
+
const bucket = authorBuckets.get(key);
|
|
400
|
+
if (bucket) bucket.push(item);
|
|
401
|
+
else authorBuckets.set(key, [item]);
|
|
402
|
+
}
|
|
403
|
+
for (const [locale, bucket] of authorBuckets) {
|
|
404
|
+
const localeOpt = locale ? { locale } : void 0;
|
|
405
|
+
const authorIds = bucket.map((i) => i.authorId).filter((id) => id !== null);
|
|
406
|
+
const uniqueAuthorIds = [...new Set(authorIds)];
|
|
407
|
+
if (uniqueAuthorIds.length === 0) continue;
|
|
408
|
+
const authorMap = await bylineRepo.findByUserIds(uniqueAuthorIds, localeOpt);
|
|
409
|
+
for (const item of bucket) {
|
|
410
|
+
if (!item.authorId) continue;
|
|
411
|
+
const f = authorMap.get(item.authorId);
|
|
412
|
+
if (f) fallbackByItem.set(item.id, f);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
for (const item of items) {
|
|
417
|
+
const explicit = bylinesByItem.get(item.id);
|
|
418
|
+
if (explicit && explicit.length > 0) {
|
|
419
|
+
item.bylines = explicit.map((c) => ({
|
|
420
|
+
...c,
|
|
421
|
+
source: "explicit"
|
|
422
|
+
}));
|
|
423
|
+
item.byline = explicit[0]?.byline ?? null;
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
const fallback = fallbackByItem.get(item.id);
|
|
427
|
+
if (fallback) {
|
|
428
|
+
item.bylines = [{
|
|
429
|
+
byline: fallback,
|
|
430
|
+
sortOrder: 0,
|
|
431
|
+
roleLabel: null,
|
|
432
|
+
source: "inferred"
|
|
433
|
+
}];
|
|
434
|
+
item.byline = fallback;
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
item.bylines = [];
|
|
438
|
+
item.byline = null;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Resolve an identifier (ID or slug) to a real content ID.
|
|
443
|
+
* Returns the ID if found, null if not found.
|
|
444
|
+
* When locale is provided, slug lookups are scoped to that locale.
|
|
445
|
+
*/
|
|
446
|
+
async function resolveId(repo, collection, identifier, locale) {
|
|
447
|
+
return (await repo.findByIdOrSlug(collection, identifier, locale ? resolveConfiguredLocale(locale) : void 0))?.id ?? null;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Resolve an identifier (ID or slug) to a real content ID,
|
|
451
|
+
* including trashed (soft-deleted) items.
|
|
452
|
+
*/
|
|
453
|
+
async function resolveIdIncludingTrashed(repo, collection, identifier, locale) {
|
|
454
|
+
return (await repo.findByIdOrSlugIncludingTrashed(collection, identifier, locale ? resolveConfiguredLocale(locale) : void 0))?.id ?? null;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Resolve the columns a content-list search should match against. Always
|
|
458
|
+
* includes `slug` (a standard column), adds the configured `titleField` plus
|
|
459
|
+
* the `title`/`name` display fields when the collection actually defines them,
|
|
460
|
+
* mirroring the admin's item-title resolution (titleField -> title -> name ->
|
|
461
|
+
* slug), and includes every field explicitly marked searchable. Returning only
|
|
462
|
+
* schema-backed columns avoids "no such column" errors.
|
|
463
|
+
*/
|
|
464
|
+
async function resolveSearchColumns(db, collection) {
|
|
465
|
+
const row = await db.selectFrom("_emdash_collections").select(["id", "title_field"]).where("slug", "=", collection).executeTakeFirst();
|
|
466
|
+
if (!row) return ["slug"];
|
|
467
|
+
const fields = await db.selectFrom("_emdash_fields").select(["slug", "searchable"]).where("collection_id", "=", row.id).orderBy("sort_order", "asc").execute();
|
|
468
|
+
const columns = new Set(["slug"]);
|
|
469
|
+
const fieldSlugs = new Set(fields.map((f) => f.slug));
|
|
470
|
+
if (row.title_field && fieldSlugs.has(row.title_field)) columns.add(row.title_field);
|
|
471
|
+
for (const candidate of ["title", "name"]) if (fieldSlugs.has(candidate)) columns.add(candidate);
|
|
472
|
+
for (const field of fields) if (field.searchable === 1) columns.add(field.slug);
|
|
473
|
+
return [...columns];
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Decide whether the content-list `q` filter can be served from the
|
|
477
|
+
* collection's FTS5 index instead of a full-scan substring LIKE (#1517).
|
|
478
|
+
*
|
|
479
|
+
* Requires SQLite (FTS5 is SQLite-only), search enabled on the collection,
|
|
480
|
+
* every non-slug display column present in the searchable-field set (or the
|
|
481
|
+
* index would miss matches the LIKE finds), and the index table actually
|
|
482
|
+
* existing.
|
|
483
|
+
*/
|
|
484
|
+
async function canUseFtsForListFilter(db, collection, searchColumns) {
|
|
485
|
+
if (!isSqlite(db)) return false;
|
|
486
|
+
const ftsManager = new FTSManager(db);
|
|
487
|
+
if (!(await ftsManager.getSearchConfig(collection))?.enabled) return false;
|
|
488
|
+
const searchable = new Set(await ftsManager.getSearchableFields(collection));
|
|
489
|
+
if (!searchColumns.every((col) => col === "slug" || searchable.has(col))) return false;
|
|
490
|
+
return ftsManager.ftsTableExists(collection);
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Create a 301 auto-redirect from an entry's old URL to its new one after a
|
|
494
|
+
* slug change, using the collection's URL pattern. Shared by
|
|
495
|
+
* handleContentUpdate (direct slug edits) and handleContentPublish (slug edits
|
|
496
|
+
* staged as `_slug` in a draft revision, which only land on publish).
|
|
497
|
+
*/
|
|
498
|
+
async function createSlugChangeRedirect(db, collection, oldSlug, newSlug, contentId, oldPublishedAt, newPublishedAt) {
|
|
499
|
+
if (await slugStillTaken(db, collection, oldSlug, contentId)) return;
|
|
500
|
+
const collectionRow = await db.selectFrom("_emdash_collections").select("url_pattern").where("slug", "=", collection).executeTakeFirst();
|
|
501
|
+
await new RedirectRepository(db).createAutoRedirect(collection, oldSlug, newSlug, contentId, collectionRow?.url_pattern ?? null, oldPublishedAt, newPublishedAt);
|
|
502
|
+
invalidateRedirectCache();
|
|
503
|
+
}
|
|
504
|
+
/** Whether a row other than `contentId` still holds `slug` in this collection. */
|
|
505
|
+
async function slugStillTaken(db, collection, slug, contentId) {
|
|
506
|
+
validateIdentifier(collection, "collection slug");
|
|
507
|
+
return (await sql`
|
|
508
|
+
SELECT id FROM ${sql.ref(`ec_${collection}`)}
|
|
509
|
+
WHERE slug = ${slug}
|
|
510
|
+
AND id != ${contentId}
|
|
511
|
+
AND deleted_at IS NULL
|
|
512
|
+
LIMIT 1
|
|
513
|
+
`.execute(db)).rows.length > 0;
|
|
514
|
+
}
|
|
515
|
+
/** Matches a date-only `YYYY-MM-DD` bound (no time component). */
|
|
516
|
+
const DATE_ONLY_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
517
|
+
/**
|
|
518
|
+
* Normalize a date-range bound to an ISO datetime for lexicographic comparison
|
|
519
|
+
* against stored ISO 8601 timestamps. A bare `YYYY-MM-DD` is widened to the
|
|
520
|
+
* appropriate UTC day boundary so the range stays inclusive: a `start` bound
|
|
521
|
+
* becomes the start of the day and an `end` bound the end of the day.
|
|
522
|
+
* Otherwise a date-only upper bound would exclude every same-day row (since
|
|
523
|
+
* `2024-06-01T12:00:00Z` sorts after `2024-06-01`). Full datetimes pass
|
|
524
|
+
* through unchanged.
|
|
525
|
+
*/
|
|
526
|
+
function normalizeDateBound(value, edge) {
|
|
527
|
+
if (!value) return void 0;
|
|
528
|
+
if (!DATE_ONLY_RE.test(value)) return new Date(value).toISOString();
|
|
529
|
+
return edge === "start" ? `${value}T00:00:00.000Z` : `${value}T23:59:59.999Z`;
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Build the repository's byline filter from the wire params.
|
|
533
|
+
*
|
|
534
|
+
* `locale` is the locale the list is scoped to, which is the locale an
|
|
535
|
+
* inferred credit has to resolve at — the admin list is always scoped to the
|
|
536
|
+
* locale picked in its switcher.
|
|
537
|
+
*/
|
|
538
|
+
function resolveBylineFilter(params, locale) {
|
|
539
|
+
const includeInferred = params.includeInferredBylines === true;
|
|
540
|
+
if (params.bylinesNone) return {
|
|
541
|
+
mode: "none",
|
|
542
|
+
includeInferred,
|
|
543
|
+
locale
|
|
544
|
+
};
|
|
545
|
+
const bylineIds = params.bylines ?? [];
|
|
546
|
+
if (bylineIds.length === 0) return void 0;
|
|
547
|
+
return {
|
|
548
|
+
mode: "any",
|
|
549
|
+
bylineIds,
|
|
550
|
+
includeInferred,
|
|
551
|
+
locale
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Create content list handler
|
|
556
|
+
*/
|
|
557
|
+
async function handleContentList(db, collection, params) {
|
|
558
|
+
try {
|
|
559
|
+
const repo = new ContentRepository(db);
|
|
560
|
+
const where = {};
|
|
561
|
+
if (params.status) where.status = params.status;
|
|
562
|
+
const locale = params.locale ? resolveConfiguredLocale(params.locale) : void 0;
|
|
563
|
+
if (locale) where.locale = locale;
|
|
564
|
+
if (params.authorId) where.authorId = params.authorId;
|
|
565
|
+
if (params.fieldFilters && Object.keys(params.fieldFilters).length > 0) where.fieldFilters = params.fieldFilters;
|
|
566
|
+
const bylineFilter = resolveBylineFilter(params, locale);
|
|
567
|
+
if (bylineFilter) where.bylineFilter = bylineFilter;
|
|
568
|
+
if (params.dateField && (params.dateFrom || params.dateTo)) where.dateFilter = {
|
|
569
|
+
field: params.dateField,
|
|
570
|
+
from: normalizeDateBound(params.dateFrom, "start"),
|
|
571
|
+
to: normalizeDateBound(params.dateTo, "end")
|
|
572
|
+
};
|
|
573
|
+
const q = params.q?.trim();
|
|
574
|
+
if (q) {
|
|
575
|
+
where.q = q;
|
|
576
|
+
where.searchColumns = await resolveSearchColumns(db, collection);
|
|
577
|
+
where.useFts = await canUseFtsForListFilter(db, collection, where.searchColumns);
|
|
578
|
+
}
|
|
579
|
+
let sortableExtras;
|
|
580
|
+
if (params.orderBy && !isSystemOrderField(params.orderBy)) {
|
|
581
|
+
const coll = await db.selectFrom("_emdash_collections").select(["title_field", "date_field"]).where("slug", "=", collection).executeTakeFirst();
|
|
582
|
+
sortableExtras = [coll?.title_field, coll?.date_field].filter((slug) => !!slug);
|
|
583
|
+
}
|
|
584
|
+
const result = await repo.findMany(collection, {
|
|
585
|
+
cursor: params.cursor,
|
|
586
|
+
limit: params.limit || 50,
|
|
587
|
+
where: Object.keys(where).length > 0 ? where : void 0,
|
|
588
|
+
orderBy: params.orderBy ? {
|
|
589
|
+
field: params.orderBy,
|
|
590
|
+
direction: params.order || "desc"
|
|
591
|
+
} : void 0,
|
|
592
|
+
sortableExtras
|
|
593
|
+
});
|
|
594
|
+
const hasSeo = await collectionHasSeo(db, collection);
|
|
595
|
+
await hydrateSeoMany(db, collection, result.items, hasSeo);
|
|
596
|
+
await hydrateBylinesMany(db, collection, result.items);
|
|
597
|
+
return {
|
|
598
|
+
success: true,
|
|
599
|
+
data: {
|
|
600
|
+
items: result.items,
|
|
601
|
+
nextCursor: result.nextCursor,
|
|
602
|
+
total: result.total
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
} catch (error) {
|
|
606
|
+
if (error instanceof InvalidCursorError) return {
|
|
607
|
+
success: false,
|
|
608
|
+
error: {
|
|
609
|
+
code: "INVALID_CURSOR",
|
|
610
|
+
message: error.message
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
if (error instanceof ContentCollectionNotFoundError || isMissingTableError(error)) return {
|
|
614
|
+
success: false,
|
|
615
|
+
error: {
|
|
616
|
+
code: "COLLECTION_NOT_FOUND",
|
|
617
|
+
message: `Collection '${collection}' not found`
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
if (isMissingColumnError(error, "deleted_at")) return {
|
|
621
|
+
success: false,
|
|
622
|
+
error: {
|
|
623
|
+
code: "COLLECTION_SCHEMA_MISMATCH",
|
|
624
|
+
message: `Collection '${collection}' backing table is missing the 'deleted_at' column`
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
if (error instanceof EmDashValidationError) return {
|
|
628
|
+
success: false,
|
|
629
|
+
error: {
|
|
630
|
+
code: "VALIDATION_ERROR",
|
|
631
|
+
message: error.message
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
console.error("Content list error:", error);
|
|
635
|
+
return {
|
|
636
|
+
success: false,
|
|
637
|
+
error: {
|
|
638
|
+
code: "CONTENT_LIST_ERROR",
|
|
639
|
+
message: "Failed to list content"
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* List the distinct authors of a collection's live content.
|
|
646
|
+
*
|
|
647
|
+
* Backs the admin content-list author filter. Unlike `/admin/users` (ADMIN
|
|
648
|
+
* only), this is gated on `content:read`, so any editor can filter by author.
|
|
649
|
+
* Returns only users who have authored at least one non-trashed entry, sorted
|
|
650
|
+
* by display name then email for a stable dropdown order.
|
|
651
|
+
*/
|
|
652
|
+
async function handleContentAuthors(db, collection) {
|
|
653
|
+
try {
|
|
654
|
+
const authorIds = await new ContentRepository(db).findDistinctAuthorIds(collection);
|
|
655
|
+
if (authorIds.length === 0) return {
|
|
656
|
+
success: true,
|
|
657
|
+
data: { items: [] }
|
|
658
|
+
};
|
|
659
|
+
return {
|
|
660
|
+
success: true,
|
|
661
|
+
data: { items: (await new UserRepository(db).findByIds(authorIds)).map((u) => ({
|
|
662
|
+
id: u.id,
|
|
663
|
+
name: u.name,
|
|
664
|
+
email: u.email,
|
|
665
|
+
avatarUrl: u.avatarUrl
|
|
666
|
+
})).toSorted((a, b) => (a.name ?? a.email).localeCompare(b.name ?? b.email)) }
|
|
667
|
+
};
|
|
668
|
+
} catch (error) {
|
|
669
|
+
if (isMissingTableError(error)) return {
|
|
670
|
+
success: false,
|
|
671
|
+
error: {
|
|
672
|
+
code: "COLLECTION_NOT_FOUND",
|
|
673
|
+
message: `Collection '${collection}' not found`
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
console.error("Content authors error:", error);
|
|
677
|
+
return {
|
|
678
|
+
success: false,
|
|
679
|
+
error: {
|
|
680
|
+
code: "CONTENT_AUTHORS_ERROR",
|
|
681
|
+
message: "Failed to list content authors"
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Get single content item
|
|
688
|
+
*/
|
|
689
|
+
async function handleContentGet(db, collection, id, locale) {
|
|
690
|
+
try {
|
|
691
|
+
const item = await new ContentRepository(db).findByIdOrSlug(collection, id, locale ? resolveConfiguredLocale(locale) : void 0);
|
|
692
|
+
if (!item) return {
|
|
693
|
+
success: false,
|
|
694
|
+
error: {
|
|
695
|
+
code: "NOT_FOUND",
|
|
696
|
+
message: `Content item not found: ${id}`
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
await hydrateSeo(db, collection, item, await collectionHasSeo(db, collection));
|
|
700
|
+
await hydrateBylines(db, collection, item);
|
|
701
|
+
return {
|
|
702
|
+
success: true,
|
|
703
|
+
data: {
|
|
704
|
+
item,
|
|
705
|
+
_rev: encodeRev(item)
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
} catch (error) {
|
|
709
|
+
console.error("Content get error:", error);
|
|
710
|
+
return {
|
|
711
|
+
success: false,
|
|
712
|
+
error: {
|
|
713
|
+
code: "CONTENT_GET_ERROR",
|
|
714
|
+
message: "Failed to get content"
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Get a content item by id, including trashed items.
|
|
721
|
+
* Used by restore endpoint for ownership checks on soft-deleted items.
|
|
722
|
+
*/
|
|
723
|
+
async function handleContentGetIncludingTrashed(db, collection, id, locale) {
|
|
724
|
+
try {
|
|
725
|
+
const item = await new ContentRepository(db).findByIdOrSlugIncludingTrashed(collection, id, locale ? resolveConfiguredLocale(locale) : void 0);
|
|
726
|
+
if (!item) return {
|
|
727
|
+
success: false,
|
|
728
|
+
error: {
|
|
729
|
+
code: "NOT_FOUND",
|
|
730
|
+
message: `Content item not found: ${id}`
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
await hydrateSeo(db, collection, item, await collectionHasSeo(db, collection));
|
|
734
|
+
await hydrateBylines(db, collection, item);
|
|
735
|
+
return {
|
|
736
|
+
success: true,
|
|
737
|
+
data: {
|
|
738
|
+
item,
|
|
739
|
+
_rev: encodeRev(item)
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
} catch (error) {
|
|
743
|
+
console.error("Content get error:", error);
|
|
744
|
+
return {
|
|
745
|
+
success: false,
|
|
746
|
+
error: {
|
|
747
|
+
code: "CONTENT_GET_ERROR",
|
|
748
|
+
message: "Failed to get content"
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Create content item.
|
|
755
|
+
*
|
|
756
|
+
* Content + SEO writes are wrapped in a transaction so either both succeed
|
|
757
|
+
* or neither does. If `body.seo` is provided for a non-SEO collection, the
|
|
758
|
+
* API returns a validation error rather than silently dropping it.
|
|
759
|
+
*/
|
|
760
|
+
async function handleContentCreate(db, collection, body) {
|
|
761
|
+
try {
|
|
762
|
+
const hasSeo = await collectionHasSeo(db, collection);
|
|
763
|
+
if (body.seo && !hasSeo) return {
|
|
764
|
+
success: false,
|
|
765
|
+
error: {
|
|
766
|
+
code: "VALIDATION_ERROR",
|
|
767
|
+
message: `Collection "${collection}" does not have SEO enabled. Remove the seo field or enable SEO on this collection.`
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
const mimeCheck = await validateMediaFields(db, collection, body.data);
|
|
771
|
+
if (!mimeCheck.success) return mimeCheck;
|
|
772
|
+
const item = await withTransaction(db, async (trx) => {
|
|
773
|
+
const repo = new ContentRepository(trx);
|
|
774
|
+
const bylineRepo = new BylineRepository(trx);
|
|
775
|
+
const inheritedFields = body.translationOf ? (await trx.selectFrom("_emdash_fields as field").innerJoin("_emdash_collections as collection", "collection.id", "field.collection_id").select("field.slug").where("collection.slug", "=", collection).where("field.translatable", "=", 0).execute()).map((field) => field.slug) : [];
|
|
776
|
+
const effectiveLocale = body.locale ? resolveConfiguredLocale(body.locale) : getI18nConfig()?.defaultLocale;
|
|
777
|
+
let slug = body.slug;
|
|
778
|
+
if (!slug) {
|
|
779
|
+
const slugSource = getSlugSource(body.data);
|
|
780
|
+
if (slugSource) slug = await repo.generateUniqueSlug(collection, slugSource, effectiveLocale);
|
|
781
|
+
}
|
|
782
|
+
if (body.status === "published") requireRoutablePublishSlug((await getCollectionPublishConfig(trx, collection)).routable, slug);
|
|
783
|
+
const created = await repo.create({
|
|
784
|
+
type: collection,
|
|
785
|
+
slug,
|
|
786
|
+
data: body.data,
|
|
787
|
+
status: body.status || "draft",
|
|
788
|
+
authorId: body.authorId,
|
|
789
|
+
locale: effectiveLocale,
|
|
790
|
+
translationOf: body.translationOf,
|
|
791
|
+
inheritFields: inheritedFields,
|
|
792
|
+
createdAt: body.createdAt,
|
|
793
|
+
publishedAt: body.publishedAt
|
|
794
|
+
});
|
|
795
|
+
if (body.bylines !== void 0) created.primaryBylineId = (await bylineRepo.setContentBylines(collection, created.id, body.bylines))[0]?.byline.translationGroup ?? null;
|
|
796
|
+
if (body.translationOf) {
|
|
797
|
+
if (body.bylines === void 0) {
|
|
798
|
+
await bylineRepo.copyContentBylines(collection, body.translationOf, created.id);
|
|
799
|
+
const source = await repo.findById(collection, body.translationOf);
|
|
800
|
+
if (source) created.primaryBylineId = source.primaryBylineId;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
await hydrateBylines(trx, collection, created);
|
|
804
|
+
if (body.seo && hasSeo) created.seo = await new SeoRepository(trx).upsert(collection, created.id, body.seo);
|
|
805
|
+
else if (hasSeo) created.seo = { ...SEO_DEFAULTS };
|
|
806
|
+
if (body.taxonomies) await assignTaxonomies(trx, collection, created.id, effectiveLocale, body.taxonomies);
|
|
807
|
+
return created;
|
|
808
|
+
});
|
|
809
|
+
return {
|
|
810
|
+
success: true,
|
|
811
|
+
data: {
|
|
812
|
+
item,
|
|
813
|
+
_rev: encodeRev(item)
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
} catch (error) {
|
|
817
|
+
if (isMissingTableError(error)) return {
|
|
818
|
+
success: false,
|
|
819
|
+
error: {
|
|
820
|
+
code: "COLLECTION_NOT_FOUND",
|
|
821
|
+
message: `Collection '${collection}' not found`
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
if (error instanceof EmDashValidationError) {
|
|
825
|
+
if (error.message === "Translation source content not found") return {
|
|
826
|
+
success: false,
|
|
827
|
+
error: {
|
|
828
|
+
code: "NOT_FOUND",
|
|
829
|
+
message: error.message
|
|
830
|
+
}
|
|
831
|
+
};
|
|
832
|
+
return {
|
|
833
|
+
success: false,
|
|
834
|
+
error: {
|
|
835
|
+
code: "VALIDATION_ERROR",
|
|
836
|
+
message: error.message
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
const message = error instanceof Error ? error.message.toLowerCase() : "";
|
|
841
|
+
if (message.includes("unique constraint failed") || message.includes("duplicate key")) {
|
|
842
|
+
if (message.includes("active_tg_locale") || message.includes("translation_group") && message.includes("locale")) return {
|
|
843
|
+
success: false,
|
|
844
|
+
error: {
|
|
845
|
+
code: "CONFLICT",
|
|
846
|
+
message: `Translation already exists in locale "${body.locale ?? getI18nConfig()?.defaultLocale ?? "en"}" for this content item`
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
if (message.includes("slug")) return {
|
|
850
|
+
success: false,
|
|
851
|
+
error: {
|
|
852
|
+
code: "SLUG_CONFLICT",
|
|
853
|
+
message: `Slug '${body.slug ?? "(auto-generated)"}' already exists in collection '${collection}'`
|
|
854
|
+
}
|
|
855
|
+
};
|
|
856
|
+
return {
|
|
857
|
+
success: false,
|
|
858
|
+
error: {
|
|
859
|
+
code: "CONFLICT",
|
|
860
|
+
message: "Unique constraint violation"
|
|
861
|
+
}
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
console.error("Content create error:", error);
|
|
865
|
+
return {
|
|
866
|
+
success: false,
|
|
867
|
+
error: {
|
|
868
|
+
code: "CONTENT_CREATE_ERROR",
|
|
869
|
+
message: "Failed to create content"
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Update content item.
|
|
876
|
+
* If `_rev` is provided, validates it against the current version before writing.
|
|
877
|
+
* No `_rev` = blind write (backwards-compatible for admin UI).
|
|
878
|
+
*
|
|
879
|
+
* Content + SEO writes are wrapped in a transaction for atomicity.
|
|
880
|
+
*/
|
|
881
|
+
async function handleContentUpdate(db, collection, id, body) {
|
|
882
|
+
try {
|
|
883
|
+
const hasSeo = await collectionHasSeo(db, collection);
|
|
884
|
+
if (body.seo && !hasSeo) return {
|
|
885
|
+
success: false,
|
|
886
|
+
error: {
|
|
887
|
+
code: "VALIDATION_ERROR",
|
|
888
|
+
message: `Collection "${collection}" does not have SEO enabled. Remove the seo field or enable SEO on this collection.`
|
|
889
|
+
}
|
|
890
|
+
};
|
|
891
|
+
if (body.data) {
|
|
892
|
+
const mimeCheck = await validateMediaFields(db, collection, body.data);
|
|
893
|
+
if (!mimeCheck.success) return mimeCheck;
|
|
894
|
+
}
|
|
895
|
+
const resolvedId = await resolveId(new ContentRepository(db), collection, id, body.locale) ?? id;
|
|
896
|
+
const item = await withTransaction(db, async (trx) => {
|
|
897
|
+
const trxRepo = new ContentRepository(trx);
|
|
898
|
+
const bylineRepo = new BylineRepository(trx);
|
|
899
|
+
const existing = body._rev || body.slug !== void 0 || body.status === "published" ? await trxRepo.findById(collection, resolvedId) : null;
|
|
900
|
+
if (body._rev) {
|
|
901
|
+
if (!existing) throw Object.assign(/* @__PURE__ */ new Error(`Content item not found: ${id}`), { apiError: { code: "NOT_FOUND" } });
|
|
902
|
+
const revCheck = validateRev(body._rev, existing);
|
|
903
|
+
if (!revCheck.valid) throw Object.assign(new Error(revCheck.message), { apiError: { code: "CONFLICT" } });
|
|
904
|
+
}
|
|
905
|
+
let oldSlug;
|
|
906
|
+
if (body.slug && existing?.slug && existing.slug !== body.slug) oldSlug = existing.slug;
|
|
907
|
+
if ((body.status ?? existing?.status) === "published") {
|
|
908
|
+
if (!existing) throw Object.assign(/* @__PURE__ */ new Error(`Content item not found: ${id}`), { apiError: { code: "NOT_FOUND" } });
|
|
909
|
+
const publishConfig = await getCollectionPublishConfig(trx, collection);
|
|
910
|
+
const intendedSlug = body.slug !== void 0 ? body.slug : existing.slug;
|
|
911
|
+
requireRoutablePublishSlug(publishConfig.routable, intendedSlug);
|
|
912
|
+
}
|
|
913
|
+
const updated = await trxRepo.update(collection, resolvedId, {
|
|
914
|
+
data: body.data,
|
|
915
|
+
slug: body.slug,
|
|
916
|
+
status: body.status,
|
|
917
|
+
authorId: body.authorId,
|
|
918
|
+
publishedAt: body.publishedAt
|
|
919
|
+
});
|
|
920
|
+
if (body.bylines !== void 0) updated.primaryBylineId = (await bylineRepo.setContentBylines(collection, resolvedId, body.bylines))[0]?.byline.translationGroup ?? null;
|
|
921
|
+
if (oldSlug && body.slug) await createSlugChangeRedirect(trx, collection, oldSlug, body.slug, resolvedId, existing?.publishedAt ?? null, updated.publishedAt ?? null);
|
|
922
|
+
if (isI18nEnabled() && body.data && updated.translationGroup) await trxRepo.syncNonTranslatableFields(collection, updated.id, updated.translationGroup, body.data);
|
|
923
|
+
if (body.seo && hasSeo) updated.seo = await new SeoRepository(trx).upsert(collection, resolvedId, body.seo);
|
|
924
|
+
else if (hasSeo) updated.seo = await new SeoRepository(trx).get(collection, resolvedId);
|
|
925
|
+
await hydrateBylines(trx, collection, updated);
|
|
926
|
+
if (body.taxonomies) await assignTaxonomies(trx, collection, resolvedId, updated.locale ?? body.locale, body.taxonomies);
|
|
927
|
+
return updated;
|
|
928
|
+
});
|
|
929
|
+
return {
|
|
930
|
+
success: true,
|
|
931
|
+
data: {
|
|
932
|
+
item,
|
|
933
|
+
_rev: encodeRev(item)
|
|
934
|
+
}
|
|
935
|
+
};
|
|
936
|
+
} catch (error) {
|
|
937
|
+
if (hasApiError(error)) return {
|
|
938
|
+
success: false,
|
|
939
|
+
error: {
|
|
940
|
+
code: error.apiError.code,
|
|
941
|
+
message: error.message
|
|
942
|
+
}
|
|
943
|
+
};
|
|
944
|
+
if (isMissingTableError(error)) return {
|
|
945
|
+
success: false,
|
|
946
|
+
error: {
|
|
947
|
+
code: "COLLECTION_NOT_FOUND",
|
|
948
|
+
message: `Collection '${collection}' not found`
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
if (error instanceof EmDashValidationError) return {
|
|
952
|
+
success: false,
|
|
953
|
+
error: {
|
|
954
|
+
code: "VALIDATION_ERROR",
|
|
955
|
+
message: error.message
|
|
956
|
+
}
|
|
957
|
+
};
|
|
958
|
+
const message = error instanceof Error ? error.message.toLowerCase() : "";
|
|
959
|
+
if (message.includes("unique constraint failed") || message.includes("duplicate key")) {
|
|
960
|
+
if (message.includes("slug")) return {
|
|
961
|
+
success: false,
|
|
962
|
+
error: {
|
|
963
|
+
code: "SLUG_CONFLICT",
|
|
964
|
+
message: `Slug '${body.slug ?? id}' already exists in collection '${collection}'`
|
|
965
|
+
}
|
|
966
|
+
};
|
|
967
|
+
return {
|
|
968
|
+
success: false,
|
|
969
|
+
error: {
|
|
970
|
+
code: "CONFLICT",
|
|
971
|
+
message: "Unique constraint violation"
|
|
972
|
+
}
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
console.error("Content update error:", error);
|
|
976
|
+
return {
|
|
977
|
+
success: false,
|
|
978
|
+
error: {
|
|
979
|
+
code: "CONTENT_UPDATE_ERROR",
|
|
980
|
+
message: "Failed to update content"
|
|
981
|
+
}
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Duplicate content item.
|
|
987
|
+
*
|
|
988
|
+
* Only copies SEO data if the collection has SEO enabled.
|
|
989
|
+
* Always returns consistent `seo` shape for SEO-enabled collections.
|
|
990
|
+
*/
|
|
991
|
+
async function handleContentDuplicate(db, collection, id, authorId) {
|
|
992
|
+
try {
|
|
993
|
+
const hasSeo = await collectionHasSeo(db, collection);
|
|
994
|
+
return {
|
|
995
|
+
success: true,
|
|
996
|
+
data: { item: await withTransaction(db, async (trx) => {
|
|
997
|
+
const repo = new ContentRepository(trx);
|
|
998
|
+
const bylineRepo = new BylineRepository(trx);
|
|
999
|
+
const resolvedId = await resolveId(repo, collection, id) ?? id;
|
|
1000
|
+
const dup = await repo.duplicate(collection, resolvedId, authorId);
|
|
1001
|
+
const existingBylines = await bylineRepo.getContentBylines(collection, resolvedId);
|
|
1002
|
+
if (existingBylines.length > 0) await bylineRepo.setContentBylines(collection, dup.id, existingBylines.map((entry) => ({
|
|
1003
|
+
bylineId: entry.byline.id,
|
|
1004
|
+
roleLabel: entry.roleLabel
|
|
1005
|
+
})));
|
|
1006
|
+
if (hasSeo) {
|
|
1007
|
+
const seoRepo = new SeoRepository(trx);
|
|
1008
|
+
await seoRepo.copyForDuplicate(collection, resolvedId, dup.id);
|
|
1009
|
+
dup.seo = await seoRepo.get(collection, dup.id);
|
|
1010
|
+
}
|
|
1011
|
+
await hydrateBylines(trx, collection, dup);
|
|
1012
|
+
return dup;
|
|
1013
|
+
}) }
|
|
1014
|
+
};
|
|
1015
|
+
} catch (err) {
|
|
1016
|
+
if (err instanceof EmDashValidationError) return {
|
|
1017
|
+
success: false,
|
|
1018
|
+
error: {
|
|
1019
|
+
code: "NOT_FOUND",
|
|
1020
|
+
message: err.message
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
1023
|
+
console.error("Content duplicate error:", err);
|
|
1024
|
+
return {
|
|
1025
|
+
success: false,
|
|
1026
|
+
error: {
|
|
1027
|
+
code: "CONTENT_DUPLICATE_ERROR",
|
|
1028
|
+
message: "Failed to duplicate content"
|
|
1029
|
+
}
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Delete content item (soft delete - moves to trash)
|
|
1035
|
+
*/
|
|
1036
|
+
async function handleContentDelete(db, collection, id) {
|
|
1037
|
+
try {
|
|
1038
|
+
const result = await withTransaction(db, async (trx) => {
|
|
1039
|
+
const repo = new ContentRepository(trx);
|
|
1040
|
+
const resolvedId = await resolveId(repo, collection, id) ?? id;
|
|
1041
|
+
const deleted = await repo.delete(collection, resolvedId);
|
|
1042
|
+
if (deleted) await new EntryLockRepository(trx).releaseEntry(collection, resolvedId);
|
|
1043
|
+
return {
|
|
1044
|
+
id: resolvedId,
|
|
1045
|
+
deleted
|
|
1046
|
+
};
|
|
1047
|
+
});
|
|
1048
|
+
if (!result.deleted) return {
|
|
1049
|
+
success: false,
|
|
1050
|
+
error: {
|
|
1051
|
+
code: "NOT_FOUND",
|
|
1052
|
+
message: `Content item not found: ${id}`
|
|
1053
|
+
}
|
|
1054
|
+
};
|
|
1055
|
+
return {
|
|
1056
|
+
success: true,
|
|
1057
|
+
data: {
|
|
1058
|
+
deleted: true,
|
|
1059
|
+
id: result.id
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
} catch (error) {
|
|
1063
|
+
console.error("Content delete error:", error);
|
|
1064
|
+
return {
|
|
1065
|
+
success: false,
|
|
1066
|
+
error: {
|
|
1067
|
+
code: "CONTENT_DELETE_ERROR",
|
|
1068
|
+
message: "Failed to delete content"
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Restore content item from trash
|
|
1075
|
+
*/
|
|
1076
|
+
async function handleContentRestore(db, collection, id, options = {}) {
|
|
1077
|
+
try {
|
|
1078
|
+
const expectedRevision = decodeRevisionPrecondition(options._rev);
|
|
1079
|
+
const item = await withTransaction(db, async (trx) => {
|
|
1080
|
+
const repo = new ContentRepository(trx);
|
|
1081
|
+
const resolvedId = await resolveIdIncludingTrashed(repo, collection, id) ?? id;
|
|
1082
|
+
return repo.restore(collection, resolvedId, expectedRevision);
|
|
1083
|
+
});
|
|
1084
|
+
if (!item) return {
|
|
1085
|
+
success: false,
|
|
1086
|
+
error: {
|
|
1087
|
+
code: "NOT_FOUND",
|
|
1088
|
+
message: `Trashed content item not found: ${id}`
|
|
1089
|
+
}
|
|
1090
|
+
};
|
|
1091
|
+
return {
|
|
1092
|
+
success: true,
|
|
1093
|
+
data: {
|
|
1094
|
+
restored: true,
|
|
1095
|
+
item,
|
|
1096
|
+
_rev: encodeRev(item)
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
} catch (error) {
|
|
1100
|
+
if (error instanceof ContentMutationConflictError) return {
|
|
1101
|
+
success: false,
|
|
1102
|
+
error: {
|
|
1103
|
+
code: "CONFLICT",
|
|
1104
|
+
message: error.message
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
if (isTranslationLocaleConflict(error, collection)) return {
|
|
1108
|
+
success: false,
|
|
1109
|
+
error: {
|
|
1110
|
+
code: "CONFLICT",
|
|
1111
|
+
message: "An active translation already exists in this locale"
|
|
1112
|
+
}
|
|
1113
|
+
};
|
|
1114
|
+
console.error("Content restore error:", error);
|
|
1115
|
+
return {
|
|
1116
|
+
success: false,
|
|
1117
|
+
error: {
|
|
1118
|
+
code: "CONTENT_RESTORE_ERROR",
|
|
1119
|
+
message: "Failed to restore content"
|
|
1120
|
+
}
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
/**
|
|
1125
|
+
* Permanently delete content item (cannot be undone).
|
|
1126
|
+
* Also cleans up associated SEO data.
|
|
1127
|
+
*/
|
|
1128
|
+
async function handleContentPermanentDelete(db, collection, id) {
|
|
1129
|
+
try {
|
|
1130
|
+
const resolvedId = await resolveIdIncludingTrashed(new ContentRepository(db), collection, id) ?? id;
|
|
1131
|
+
if (!await withTransaction(db, async (trx) => {
|
|
1132
|
+
const trxRepo = new ContentRepository(trx);
|
|
1133
|
+
const item = await trxRepo.findByIdIncludingTrashed(collection, resolvedId);
|
|
1134
|
+
const wasDeleted = await trxRepo.permanentDelete(collection, resolvedId);
|
|
1135
|
+
if (wasDeleted) {
|
|
1136
|
+
await new SeoRepository(trx).delete(collection, resolvedId);
|
|
1137
|
+
await new CommentRepository(trx).deleteByContent(collection, resolvedId);
|
|
1138
|
+
await new RevisionRepository(trx).deleteByEntry(collection, resolvedId);
|
|
1139
|
+
await new EntryLockRepository(trx).releaseEntry(collection, resolvedId);
|
|
1140
|
+
await new BylineRepository(trx).deleteContentBylines(collection, resolvedId);
|
|
1141
|
+
if (item?.translationGroup) {
|
|
1142
|
+
if (!await trxRepo.hasTranslationsIncludingTrashed(collection, item.translationGroup)) await new TaxonomyRepository(trx).clearEntryGroupTerms(collection, item.translationGroup);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
return wasDeleted;
|
|
1146
|
+
})) return {
|
|
1147
|
+
success: false,
|
|
1148
|
+
error: {
|
|
1149
|
+
code: "NOT_FOUND",
|
|
1150
|
+
message: `Content item not found: ${id}`
|
|
1151
|
+
}
|
|
1152
|
+
};
|
|
1153
|
+
return {
|
|
1154
|
+
success: true,
|
|
1155
|
+
data: {
|
|
1156
|
+
deleted: true,
|
|
1157
|
+
id: resolvedId
|
|
1158
|
+
}
|
|
1159
|
+
};
|
|
1160
|
+
} catch (error) {
|
|
1161
|
+
console.error("Content permanent delete error:", error);
|
|
1162
|
+
return {
|
|
1163
|
+
success: false,
|
|
1164
|
+
error: {
|
|
1165
|
+
code: "CONTENT_DELETE_ERROR",
|
|
1166
|
+
message: "Failed to permanently delete content"
|
|
1167
|
+
}
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* List trashed content items
|
|
1173
|
+
*/
|
|
1174
|
+
async function handleContentListTrashed(db, collection, options = {}) {
|
|
1175
|
+
try {
|
|
1176
|
+
const result = await new ContentRepository(db).findTrashed(collection, {
|
|
1177
|
+
limit: options.limit,
|
|
1178
|
+
cursor: options.cursor,
|
|
1179
|
+
where: { locale: options.locale }
|
|
1180
|
+
});
|
|
1181
|
+
return {
|
|
1182
|
+
success: true,
|
|
1183
|
+
data: {
|
|
1184
|
+
items: result.items.map((item) => ({
|
|
1185
|
+
id: item.id,
|
|
1186
|
+
type: item.type,
|
|
1187
|
+
slug: item.slug,
|
|
1188
|
+
status: item.status,
|
|
1189
|
+
locale: item.locale,
|
|
1190
|
+
translationGroup: item.translationGroup,
|
|
1191
|
+
data: item.data,
|
|
1192
|
+
authorId: item.authorId,
|
|
1193
|
+
createdAt: item.createdAt,
|
|
1194
|
+
updatedAt: item.updatedAt,
|
|
1195
|
+
publishedAt: item.publishedAt,
|
|
1196
|
+
deletedAt: item.deletedAt
|
|
1197
|
+
})),
|
|
1198
|
+
nextCursor: result.nextCursor
|
|
1199
|
+
}
|
|
1200
|
+
};
|
|
1201
|
+
} catch (error) {
|
|
1202
|
+
if (error instanceof InvalidCursorError) return {
|
|
1203
|
+
success: false,
|
|
1204
|
+
error: {
|
|
1205
|
+
code: "INVALID_CURSOR",
|
|
1206
|
+
message: error.message
|
|
1207
|
+
}
|
|
1208
|
+
};
|
|
1209
|
+
console.error("Content list trashed error:", error);
|
|
1210
|
+
return {
|
|
1211
|
+
success: false,
|
|
1212
|
+
error: {
|
|
1213
|
+
code: "CONTENT_LIST_ERROR",
|
|
1214
|
+
message: "Failed to list trashed content"
|
|
1215
|
+
}
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* Count trashed content items
|
|
1221
|
+
*/
|
|
1222
|
+
async function handleContentCountTrashed(db, collection, options = {}) {
|
|
1223
|
+
try {
|
|
1224
|
+
return {
|
|
1225
|
+
success: true,
|
|
1226
|
+
data: { count: await new ContentRepository(db).countTrashed(collection, { locale: options.locale }) }
|
|
1227
|
+
};
|
|
1228
|
+
} catch (error) {
|
|
1229
|
+
console.error("Content count trashed error:", error);
|
|
1230
|
+
return {
|
|
1231
|
+
success: false,
|
|
1232
|
+
error: {
|
|
1233
|
+
code: "CONTENT_COUNT_ERROR",
|
|
1234
|
+
message: "Failed to count trashed content"
|
|
1235
|
+
}
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* Schedule content for future publishing
|
|
1241
|
+
*/
|
|
1242
|
+
async function handleContentSchedule(db, collection, id, scheduledAt, currentTime = /* @__PURE__ */ new Date(), _rev) {
|
|
1243
|
+
try {
|
|
1244
|
+
const expectedRevision = decodeRevisionPrecondition(_rev);
|
|
1245
|
+
const item = await withTransaction(db, async (trx) => {
|
|
1246
|
+
const repo = new ContentRepository(trx);
|
|
1247
|
+
const existing = await repo.findByIdOrSlug(collection, id);
|
|
1248
|
+
const resolvedId = existing?.id ?? id;
|
|
1249
|
+
if (existing) requireRoutablePublishSlug((await getCollectionPublishConfig(trx, collection)).routable, existing.slug);
|
|
1250
|
+
return repo.schedule(collection, resolvedId, scheduledAt, currentTime, expectedRevision);
|
|
1251
|
+
});
|
|
1252
|
+
await hydrateSeo(db, collection, item, await collectionHasSeo(db, collection));
|
|
1253
|
+
return {
|
|
1254
|
+
success: true,
|
|
1255
|
+
data: {
|
|
1256
|
+
item,
|
|
1257
|
+
_rev: encodeRev(item)
|
|
1258
|
+
}
|
|
1259
|
+
};
|
|
1260
|
+
} catch (error) {
|
|
1261
|
+
if (error instanceof ContentMutationConflictError) return {
|
|
1262
|
+
success: false,
|
|
1263
|
+
error: {
|
|
1264
|
+
code: "CONFLICT",
|
|
1265
|
+
message: error.message
|
|
1266
|
+
}
|
|
1267
|
+
};
|
|
1268
|
+
if (error instanceof EmDashValidationError) return {
|
|
1269
|
+
success: false,
|
|
1270
|
+
error: {
|
|
1271
|
+
code: "VALIDATION_ERROR",
|
|
1272
|
+
message: error.message
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
1275
|
+
console.error("Content schedule error:", error);
|
|
1276
|
+
return {
|
|
1277
|
+
success: false,
|
|
1278
|
+
error: {
|
|
1279
|
+
code: "CONTENT_SCHEDULE_ERROR",
|
|
1280
|
+
message: "Failed to schedule content"
|
|
1281
|
+
}
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
* Unschedule content (revert to draft)
|
|
1287
|
+
*/
|
|
1288
|
+
async function handleContentUnschedule(db, collection, id, options = {}) {
|
|
1289
|
+
try {
|
|
1290
|
+
const expectedRevision = decodeRevisionPrecondition(options._rev);
|
|
1291
|
+
const item = await withTransaction(db, async (trx) => {
|
|
1292
|
+
const repo = new ContentRepository(trx);
|
|
1293
|
+
const resolvedId = await resolveId(repo, collection, id) ?? id;
|
|
1294
|
+
return repo.unschedule(collection, resolvedId, expectedRevision);
|
|
1295
|
+
});
|
|
1296
|
+
await hydrateSeo(db, collection, item, await collectionHasSeo(db, collection));
|
|
1297
|
+
return {
|
|
1298
|
+
success: true,
|
|
1299
|
+
data: {
|
|
1300
|
+
item,
|
|
1301
|
+
_rev: encodeRev(item)
|
|
1302
|
+
}
|
|
1303
|
+
};
|
|
1304
|
+
} catch (error) {
|
|
1305
|
+
if (error instanceof ContentMutationConflictError) return {
|
|
1306
|
+
success: false,
|
|
1307
|
+
error: {
|
|
1308
|
+
code: "CONFLICT",
|
|
1309
|
+
message: error.message
|
|
1310
|
+
}
|
|
1311
|
+
};
|
|
1312
|
+
if (error instanceof EmDashValidationError) return {
|
|
1313
|
+
success: false,
|
|
1314
|
+
error: {
|
|
1315
|
+
code: "VALIDATION_ERROR",
|
|
1316
|
+
message: error.message
|
|
1317
|
+
}
|
|
1318
|
+
};
|
|
1319
|
+
console.error("Content unschedule error:", error);
|
|
1320
|
+
return {
|
|
1321
|
+
success: false,
|
|
1322
|
+
error: {
|
|
1323
|
+
code: "CONTENT_UNSCHEDULE_ERROR",
|
|
1324
|
+
message: "Failed to unschedule content"
|
|
1325
|
+
}
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Persist a permanent policy rejection and unschedule the publication.
|
|
1331
|
+
* Databases with transactions commit both writes together. D1 persists the
|
|
1332
|
+
* reason first so a failed option write cannot silently remove the entry from
|
|
1333
|
+
* future sweeps; if unscheduling then fails, the due entry and its dashboard
|
|
1334
|
+
* notice remain available for retry and operator action.
|
|
1335
|
+
*/
|
|
1336
|
+
async function handleScheduledPolicyRejection(db, collection, id, options) {
|
|
1337
|
+
try {
|
|
1338
|
+
const expectedRevision = decodeRevisionPrecondition(options._rev);
|
|
1339
|
+
const item = await withTransaction(db, async (trx) => {
|
|
1340
|
+
const repo = new ContentRepository(trx);
|
|
1341
|
+
const resolvedId = await resolveId(repo, collection, id) ?? id;
|
|
1342
|
+
const optionsRepo = new OptionsRepository(trx);
|
|
1343
|
+
const rejectionKey = scheduledPolicyRejectionKey(collection, resolvedId);
|
|
1344
|
+
const rejectionRevision = await optionsRepo.setVersioned(rejectionKey, {
|
|
1345
|
+
...options.rejection,
|
|
1346
|
+
id: resolvedId
|
|
1347
|
+
});
|
|
1348
|
+
try {
|
|
1349
|
+
return await repo.unschedule(collection, resolvedId, expectedRevision);
|
|
1350
|
+
} catch (error) {
|
|
1351
|
+
if (error instanceof ContentMutationConflictError) try {
|
|
1352
|
+
await optionsRepo.compareAndDelete(rejectionKey, rejectionRevision);
|
|
1353
|
+
} catch (cleanupError) {
|
|
1354
|
+
console.error("Failed to clear stale scheduled policy rejection:", cleanupError);
|
|
1355
|
+
}
|
|
1356
|
+
throw error;
|
|
1357
|
+
}
|
|
1358
|
+
});
|
|
1359
|
+
await hydrateSeo(db, collection, item, await collectionHasSeo(db, collection));
|
|
1360
|
+
return {
|
|
1361
|
+
success: true,
|
|
1362
|
+
data: {
|
|
1363
|
+
item,
|
|
1364
|
+
_rev: encodeRev(item)
|
|
1365
|
+
}
|
|
1366
|
+
};
|
|
1367
|
+
} catch (error) {
|
|
1368
|
+
if (error instanceof ContentMutationConflictError) return {
|
|
1369
|
+
success: false,
|
|
1370
|
+
error: {
|
|
1371
|
+
code: "CONFLICT",
|
|
1372
|
+
message: error.message
|
|
1373
|
+
}
|
|
1374
|
+
};
|
|
1375
|
+
if (error instanceof EmDashValidationError) return {
|
|
1376
|
+
success: false,
|
|
1377
|
+
error: {
|
|
1378
|
+
code: "VALIDATION_ERROR",
|
|
1379
|
+
message: error.message
|
|
1380
|
+
}
|
|
1381
|
+
};
|
|
1382
|
+
console.error("Scheduled policy rejection error:", error);
|
|
1383
|
+
return {
|
|
1384
|
+
success: false,
|
|
1385
|
+
error: {
|
|
1386
|
+
code: "CONTENT_UNSCHEDULE_ERROR",
|
|
1387
|
+
message: "Failed to record scheduled publication rejection"
|
|
1388
|
+
}
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
/**
|
|
1393
|
+
* Publish content immediately.
|
|
1394
|
+
*
|
|
1395
|
+
* Publication is one atomic content-row statement. On databases that support
|
|
1396
|
+
* transactions, the slug redirect and the locale sync stay grouped with it.
|
|
1397
|
+
*/
|
|
1398
|
+
async function handleContentPublish(db, collection, id, options = {}) {
|
|
1399
|
+
try {
|
|
1400
|
+
const expectedRevision = decodeRevisionPrecondition(options._rev);
|
|
1401
|
+
const item = await withTransaction(db, async (trx) => {
|
|
1402
|
+
const repo = new ContentRepository(trx);
|
|
1403
|
+
const resolvedId = await resolveId(repo, collection, id) ?? id;
|
|
1404
|
+
const publishConfig = await getCollectionPublishConfig(trx, collection);
|
|
1405
|
+
const existing = await repo.findById(collection, resolvedId);
|
|
1406
|
+
const published = await repo.publish(collection, resolvedId, options.publishedAt, options.requireScheduledDue, options.expectedScheduledAt, publishConfig.supportsRevisions, publishConfig.routable, expectedRevision, options.currentTime);
|
|
1407
|
+
if (existing && isI18nEnabled() && publishConfig.supportsRevisions && published.translationGroup) await repo.syncNonTranslatableFields(collection, published.id, published.translationGroup, published.data, { previous: existing.data });
|
|
1408
|
+
if (existing?.status === "published" && existing.slug && published.slug && existing.slug !== published.slug) await createSlugChangeRedirect(trx, collection, existing.slug, published.slug, resolvedId, existing.publishedAt ?? null, published.publishedAt ?? null);
|
|
1409
|
+
return published;
|
|
1410
|
+
});
|
|
1411
|
+
await hydrateSeo(db, collection, item, await collectionHasSeo(db, collection));
|
|
1412
|
+
return {
|
|
1413
|
+
success: true,
|
|
1414
|
+
data: {
|
|
1415
|
+
item,
|
|
1416
|
+
_rev: encodeRev(item)
|
|
1417
|
+
}
|
|
1418
|
+
};
|
|
1419
|
+
} catch (error) {
|
|
1420
|
+
if (error instanceof ContentMutationConflictError) return {
|
|
1421
|
+
success: false,
|
|
1422
|
+
error: {
|
|
1423
|
+
code: "CONFLICT",
|
|
1424
|
+
message: error.message
|
|
1425
|
+
}
|
|
1426
|
+
};
|
|
1427
|
+
if (error instanceof ScheduledNotDueError) return {
|
|
1428
|
+
success: false,
|
|
1429
|
+
error: {
|
|
1430
|
+
code: "NOT_DUE",
|
|
1431
|
+
message: error.message
|
|
1432
|
+
}
|
|
1433
|
+
};
|
|
1434
|
+
if (error instanceof EmDashValidationError) {
|
|
1435
|
+
const details = error.details;
|
|
1436
|
+
return {
|
|
1437
|
+
success: false,
|
|
1438
|
+
error: {
|
|
1439
|
+
code: typeof details === "object" && details !== null && "code" in details && details.code === "SLUG_CONFLICT" ? "SLUG_CONFLICT" : "VALIDATION_ERROR",
|
|
1440
|
+
message: error.message
|
|
1441
|
+
}
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
const message = error instanceof Error ? error.message.toLowerCase() : "";
|
|
1445
|
+
if ((message.includes("unique constraint failed") || message.includes("duplicate key")) && message.includes("slug")) return {
|
|
1446
|
+
success: false,
|
|
1447
|
+
error: {
|
|
1448
|
+
code: "SLUG_CONFLICT",
|
|
1449
|
+
message: `The staged slug is already used by another entry in collection '${collection}'`
|
|
1450
|
+
}
|
|
1451
|
+
};
|
|
1452
|
+
console.error("Content publish error:", error);
|
|
1453
|
+
return {
|
|
1454
|
+
success: false,
|
|
1455
|
+
error: {
|
|
1456
|
+
code: "CONTENT_PUBLISH_ERROR",
|
|
1457
|
+
message: "Failed to publish content"
|
|
1458
|
+
}
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
/**
|
|
1463
|
+
* Unpublish content (revert to draft).
|
|
1464
|
+
*
|
|
1465
|
+
* Wrapped in a transaction — unpublish may create a draft revision
|
|
1466
|
+
* from the live version then update the status, which is multi-step.
|
|
1467
|
+
*/
|
|
1468
|
+
async function handleContentUnpublish(db, collection, id, options = {}) {
|
|
1469
|
+
try {
|
|
1470
|
+
const expectedRevision = decodeRevisionPrecondition(options._rev);
|
|
1471
|
+
const item = await withTransaction(db, async (trx) => {
|
|
1472
|
+
const repo = new ContentRepository(trx);
|
|
1473
|
+
const resolvedId = await resolveId(repo, collection, id) ?? id;
|
|
1474
|
+
return repo.unpublish(collection, resolvedId, expectedRevision);
|
|
1475
|
+
});
|
|
1476
|
+
await hydrateSeo(db, collection, item, await collectionHasSeo(db, collection));
|
|
1477
|
+
return {
|
|
1478
|
+
success: true,
|
|
1479
|
+
data: {
|
|
1480
|
+
item,
|
|
1481
|
+
_rev: encodeRev(item)
|
|
1482
|
+
}
|
|
1483
|
+
};
|
|
1484
|
+
} catch (error) {
|
|
1485
|
+
if (error instanceof ContentMutationConflictError) return {
|
|
1486
|
+
success: false,
|
|
1487
|
+
error: {
|
|
1488
|
+
code: "CONFLICT",
|
|
1489
|
+
message: error.message
|
|
1490
|
+
}
|
|
1491
|
+
};
|
|
1492
|
+
if (error instanceof EmDashValidationError) return {
|
|
1493
|
+
success: false,
|
|
1494
|
+
error: {
|
|
1495
|
+
code: "VALIDATION_ERROR",
|
|
1496
|
+
message: error.message
|
|
1497
|
+
}
|
|
1498
|
+
};
|
|
1499
|
+
console.error("Content unpublish error:", error);
|
|
1500
|
+
return {
|
|
1501
|
+
success: false,
|
|
1502
|
+
error: {
|
|
1503
|
+
code: "CONTENT_UNPUBLISH_ERROR",
|
|
1504
|
+
message: "Failed to unpublish content"
|
|
1505
|
+
}
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* Count scheduled content items
|
|
1511
|
+
*/
|
|
1512
|
+
async function handleContentCountScheduled(db, collection) {
|
|
1513
|
+
try {
|
|
1514
|
+
return {
|
|
1515
|
+
success: true,
|
|
1516
|
+
data: { count: await new ContentRepository(db).countScheduled(collection) }
|
|
1517
|
+
};
|
|
1518
|
+
} catch (error) {
|
|
1519
|
+
console.error("Content count scheduled error:", error);
|
|
1520
|
+
return {
|
|
1521
|
+
success: false,
|
|
1522
|
+
error: {
|
|
1523
|
+
code: "CONTENT_COUNT_ERROR",
|
|
1524
|
+
message: "Failed to count scheduled content"
|
|
1525
|
+
}
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
/**
|
|
1530
|
+
* Discard draft changes (revert to live version)
|
|
1531
|
+
*/
|
|
1532
|
+
async function handleContentDiscardDraft(db, collection, id, options = {}) {
|
|
1533
|
+
try {
|
|
1534
|
+
const expectedRevision = decodeRevisionPrecondition(options._rev);
|
|
1535
|
+
const item = await withTransaction(db, async (trx) => {
|
|
1536
|
+
const repo = new ContentRepository(trx);
|
|
1537
|
+
const resolvedId = await resolveId(repo, collection, id) ?? id;
|
|
1538
|
+
return repo.discardDraft(collection, resolvedId, expectedRevision);
|
|
1539
|
+
});
|
|
1540
|
+
await hydrateSeo(db, collection, item, await collectionHasSeo(db, collection));
|
|
1541
|
+
return {
|
|
1542
|
+
success: true,
|
|
1543
|
+
data: {
|
|
1544
|
+
item,
|
|
1545
|
+
_rev: encodeRev(item)
|
|
1546
|
+
}
|
|
1547
|
+
};
|
|
1548
|
+
} catch (error) {
|
|
1549
|
+
if (error instanceof ContentMutationConflictError) return {
|
|
1550
|
+
success: false,
|
|
1551
|
+
error: {
|
|
1552
|
+
code: "CONFLICT",
|
|
1553
|
+
message: error.message
|
|
1554
|
+
}
|
|
1555
|
+
};
|
|
1556
|
+
if (error instanceof EmDashValidationError) return {
|
|
1557
|
+
success: false,
|
|
1558
|
+
error: {
|
|
1559
|
+
code: "NOT_FOUND",
|
|
1560
|
+
message: error.message
|
|
1561
|
+
}
|
|
1562
|
+
};
|
|
1563
|
+
console.error("Content discard draft error:", error);
|
|
1564
|
+
return {
|
|
1565
|
+
success: false,
|
|
1566
|
+
error: {
|
|
1567
|
+
code: "CONTENT_DISCARD_DRAFT_ERROR",
|
|
1568
|
+
message: "Failed to discard draft"
|
|
1569
|
+
}
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* Compare live and draft revisions
|
|
1575
|
+
*/
|
|
1576
|
+
async function handleContentCompare(db, collection, id) {
|
|
1577
|
+
try {
|
|
1578
|
+
const entry = await new ContentRepository(db).findByIdOrSlug(collection, id);
|
|
1579
|
+
if (!entry) return {
|
|
1580
|
+
success: false,
|
|
1581
|
+
error: {
|
|
1582
|
+
code: "NOT_FOUND",
|
|
1583
|
+
message: `Content item not found: ${id}`
|
|
1584
|
+
}
|
|
1585
|
+
};
|
|
1586
|
+
const revisionRepo = new RevisionRepository(db);
|
|
1587
|
+
const live = entry.liveRevisionId ? await revisionRepo.findById(entry.liveRevisionId) : null;
|
|
1588
|
+
const draft = entry.draftRevisionId ? await revisionRepo.findById(entry.draftRevisionId) : null;
|
|
1589
|
+
return {
|
|
1590
|
+
success: true,
|
|
1591
|
+
data: {
|
|
1592
|
+
hasChanges: entry.draftRevisionId !== null && entry.draftRevisionId !== entry.liveRevisionId,
|
|
1593
|
+
live: live?.data ?? null,
|
|
1594
|
+
draft: draft?.data ?? null
|
|
1595
|
+
}
|
|
1596
|
+
};
|
|
1597
|
+
} catch (error) {
|
|
1598
|
+
console.error("Content compare error:", error);
|
|
1599
|
+
return {
|
|
1600
|
+
success: false,
|
|
1601
|
+
error: {
|
|
1602
|
+
code: "CONTENT_COMPARE_ERROR",
|
|
1603
|
+
message: "Failed to compare revisions"
|
|
1604
|
+
}
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
/**
|
|
1609
|
+
* Get all translations for a content item.
|
|
1610
|
+
* Returns the item's translation group members with locale and status info.
|
|
1611
|
+
*/
|
|
1612
|
+
async function handleContentTranslations(db, collection, id) {
|
|
1613
|
+
try {
|
|
1614
|
+
const repo = new ContentRepository(db);
|
|
1615
|
+
const item = await repo.findByIdOrSlug(collection, id);
|
|
1616
|
+
if (!item) return {
|
|
1617
|
+
success: false,
|
|
1618
|
+
error: {
|
|
1619
|
+
code: "NOT_FOUND",
|
|
1620
|
+
message: `Content item not found: ${id}`
|
|
1621
|
+
}
|
|
1622
|
+
};
|
|
1623
|
+
if (!item.translationGroup) return {
|
|
1624
|
+
success: true,
|
|
1625
|
+
data: {
|
|
1626
|
+
translationGroup: item.id,
|
|
1627
|
+
translations: [{
|
|
1628
|
+
id: item.id,
|
|
1629
|
+
locale: item.locale,
|
|
1630
|
+
slug: item.slug,
|
|
1631
|
+
status: item.status,
|
|
1632
|
+
updatedAt: item.updatedAt
|
|
1633
|
+
}]
|
|
1634
|
+
}
|
|
1635
|
+
};
|
|
1636
|
+
const translations = await repo.findTranslations(collection, item.translationGroup);
|
|
1637
|
+
return {
|
|
1638
|
+
success: true,
|
|
1639
|
+
data: {
|
|
1640
|
+
translationGroup: item.translationGroup,
|
|
1641
|
+
translations: translations.map((t) => ({
|
|
1642
|
+
id: t.id,
|
|
1643
|
+
locale: t.locale,
|
|
1644
|
+
slug: t.slug,
|
|
1645
|
+
status: t.status,
|
|
1646
|
+
updatedAt: t.updatedAt
|
|
1647
|
+
}))
|
|
1648
|
+
}
|
|
1649
|
+
};
|
|
1650
|
+
} catch (error) {
|
|
1651
|
+
if (error instanceof Error) console.error("Content translations error:", error);
|
|
1652
|
+
return {
|
|
1653
|
+
success: false,
|
|
1654
|
+
error: {
|
|
1655
|
+
code: "CONTENT_TRANSLATIONS_ERROR",
|
|
1656
|
+
message: "Failed to get translations"
|
|
1657
|
+
}
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
/**
|
|
1662
|
+
* Resolve a `{ taxonomyName: [slug, ...] }` map to term IDs and replace the
|
|
1663
|
+
* entry's assignments for each named taxonomy.
|
|
1664
|
+
*
|
|
1665
|
+
* Shared by handleContentCreate and handleContentUpdate so both MCP entry
|
|
1666
|
+
* points behave identically. Slug resolution is scoped to `locale`; passing
|
|
1667
|
+
* `undefined` lets `findBySlug` fall back to its default (lowest locale code)
|
|
1668
|
+
* so callers on single-locale sites don't need to know the site's default.
|
|
1669
|
+
*
|
|
1670
|
+
* Throws EmDashValidationError on unknown slug or wrong shape; the calling
|
|
1671
|
+
* handler translates that into a VALIDATION_ERROR response.
|
|
1672
|
+
*/
|
|
1673
|
+
async function assignTaxonomies(trx, collection, entryId, locale, taxonomies) {
|
|
1674
|
+
const taxRepo = new TaxonomyRepository(trx);
|
|
1675
|
+
let anyChange = false;
|
|
1676
|
+
for (const [taxonomyName, slugs] of Object.entries(taxonomies)) {
|
|
1677
|
+
if (!Array.isArray(slugs)) throw new EmDashValidationError(`taxonomies.${taxonomyName} must be an array of term slugs`);
|
|
1678
|
+
const termIds = [];
|
|
1679
|
+
for (const slug of slugs) {
|
|
1680
|
+
if (typeof slug !== "string" || slug.length === 0) throw new EmDashValidationError(`taxonomies.${taxonomyName} contains a non-string or empty slug`);
|
|
1681
|
+
const term = await taxRepo.findBySlug(taxonomyName, slug, locale);
|
|
1682
|
+
if (!term) throw new EmDashValidationError(`Unknown taxonomy term: ${taxonomyName}='${slug}'${locale ? ` (locale '${locale}')` : ""}`);
|
|
1683
|
+
termIds.push(term.id);
|
|
1684
|
+
}
|
|
1685
|
+
await taxRepo.setTermsForEntry(collection, entryId, taxonomyName, termIds);
|
|
1686
|
+
anyChange = true;
|
|
1687
|
+
}
|
|
1688
|
+
if (anyChange) invalidateTermCache();
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
//#endregion
|
|
1692
|
+
//#region src/api/handlers/manifest.ts
|
|
1693
|
+
/** Pattern to add spaces before capital letters */
|
|
1694
|
+
const CAMEL_CASE_PATTERN = /([A-Z])/g;
|
|
1695
|
+
const FIRST_CHAR_PATTERN = /^./;
|
|
1696
|
+
/**
|
|
1697
|
+
* Map schema field types to editor field kinds.
|
|
1698
|
+
*/
|
|
1699
|
+
/** Field types that can be surfaced as list columns in the admin. */
|
|
1700
|
+
const LIST_COLUMN_FIELD_TYPES = new Set([
|
|
1701
|
+
"string",
|
|
1702
|
+
"number",
|
|
1703
|
+
"integer",
|
|
1704
|
+
"boolean",
|
|
1705
|
+
"datetime",
|
|
1706
|
+
"select",
|
|
1707
|
+
"multiSelect"
|
|
1708
|
+
]);
|
|
1709
|
+
const FIELD_TYPE_TO_KIND = {
|
|
1710
|
+
string: "string",
|
|
1711
|
+
slug: "string",
|
|
1712
|
+
url: "url",
|
|
1713
|
+
text: "richText",
|
|
1714
|
+
number: "number",
|
|
1715
|
+
integer: "number",
|
|
1716
|
+
boolean: "boolean",
|
|
1717
|
+
datetime: "datetime",
|
|
1718
|
+
select: "select",
|
|
1719
|
+
multiSelect: "multiSelect",
|
|
1720
|
+
portableText: "portableText",
|
|
1721
|
+
image: "image",
|
|
1722
|
+
file: "file",
|
|
1723
|
+
reference: "reference",
|
|
1724
|
+
json: "json",
|
|
1725
|
+
repeater: "repeater",
|
|
1726
|
+
blocks: "blocks"
|
|
1727
|
+
};
|
|
1728
|
+
/**
|
|
1729
|
+
* Generate admin manifest from collections
|
|
1730
|
+
*/
|
|
1731
|
+
async function generateManifest(collections, plugins = {}, options = {}) {
|
|
1732
|
+
const manifestCollections = await buildManifestCollections(collections, options.db);
|
|
1733
|
+
return {
|
|
1734
|
+
version: "0.1.0",
|
|
1735
|
+
hash: await hashString(JSON.stringify(manifestCollections)),
|
|
1736
|
+
collections: manifestCollections,
|
|
1737
|
+
plugins
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
/**
|
|
1741
|
+
* Build collection descriptors from build-time config plus live database rows.
|
|
1742
|
+
*
|
|
1743
|
+
* Config collections are added first and win on slug conflicts. Runtime/manual
|
|
1744
|
+
* collections have no Zod schema to inspect, so their field descriptors are
|
|
1745
|
+
* synthesized from `_emdash_fields`.
|
|
1746
|
+
*/
|
|
1747
|
+
async function buildManifestCollections(collections, db) {
|
|
1748
|
+
const manifestCollections = {};
|
|
1749
|
+
for (const [name, definition] of Object.entries(collections)) {
|
|
1750
|
+
const fields = extractFieldDescriptors(definition.schema);
|
|
1751
|
+
manifestCollections[name] = {
|
|
1752
|
+
label: definition.admin.label,
|
|
1753
|
+
labelSingular: definition.admin.labelSingular || definition.admin.label,
|
|
1754
|
+
supports: definition.admin.supports || [],
|
|
1755
|
+
hasSeo: (definition.admin.supports || []).includes("seo"),
|
|
1756
|
+
routable: definition.admin.routable ?? true,
|
|
1757
|
+
fields
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
if (!db) return manifestCollections;
|
|
1761
|
+
try {
|
|
1762
|
+
const storedCollections = await new SchemaRegistry(db).listCollectionsWithFields();
|
|
1763
|
+
const dbCollections = await Promise.all(storedCollections.map((collection) => expandCollectionBlockFields(db, collection)));
|
|
1764
|
+
for (const collection of dbCollections) {
|
|
1765
|
+
if (manifestCollections[collection.slug]) continue;
|
|
1766
|
+
const fields = {};
|
|
1767
|
+
for (const field of collection.fields) fields[field.slug] = dbFieldDescriptor(field);
|
|
1768
|
+
const configuredListColumns = collection.admin?.listColumns ?? [];
|
|
1769
|
+
const fieldTypes = new Map(collection.fields.map((field) => [field.slug, field.type]));
|
|
1770
|
+
const listColumns = [];
|
|
1771
|
+
for (const slug of configuredListColumns) {
|
|
1772
|
+
if (listColumns.includes(slug)) continue;
|
|
1773
|
+
const fieldType = fieldTypes.get(slug);
|
|
1774
|
+
if (!fieldType || !LIST_COLUMN_FIELD_TYPES.has(fieldType)) {
|
|
1775
|
+
console.warn(`EmDash: Ignoring unsupported or unknown list column "${slug}" in collection "${collection.slug}".`);
|
|
1776
|
+
continue;
|
|
1777
|
+
}
|
|
1778
|
+
if (listColumns.length >= MAX_COLLECTION_LIST_COLUMNS) {
|
|
1779
|
+
console.warn(`EmDash: Collection "${collection.slug}" declares more than ${MAX_COLLECTION_LIST_COLUMNS} list columns; extra columns are ignored.`);
|
|
1780
|
+
break;
|
|
1781
|
+
}
|
|
1782
|
+
listColumns.push(slug);
|
|
1783
|
+
}
|
|
1784
|
+
manifestCollections[collection.slug] = {
|
|
1785
|
+
label: collection.label,
|
|
1786
|
+
labelSingular: collection.labelSingular || collection.label,
|
|
1787
|
+
supports: collection.supports || [],
|
|
1788
|
+
hasSeo: collection.hasSeo,
|
|
1789
|
+
urlPattern: collection.urlPattern,
|
|
1790
|
+
routable: collection.routable !== false,
|
|
1791
|
+
titleField: collection.titleField,
|
|
1792
|
+
dateField: collection.dateField,
|
|
1793
|
+
...collection.hidden ? { hidden: true } : {},
|
|
1794
|
+
...collection.group ? { group: collection.group } : {},
|
|
1795
|
+
listColumns: listColumns.length > 0 ? listColumns : void 0,
|
|
1796
|
+
fields
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
} catch (error) {
|
|
1800
|
+
console.debug("EmDash: Could not load database collections for manifest:", error);
|
|
1801
|
+
}
|
|
1802
|
+
return manifestCollections;
|
|
1803
|
+
}
|
|
1804
|
+
/**
|
|
1805
|
+
* Extract field descriptors from Zod schema
|
|
1806
|
+
* Note: This is a simplified implementation that handles common types
|
|
1807
|
+
*/
|
|
1808
|
+
function extractFieldDescriptors(schema) {
|
|
1809
|
+
const fields = {};
|
|
1810
|
+
const shape = typeof schema._def?.shape === "function" ? schema._def.shape() : schema.shape || {};
|
|
1811
|
+
for (const [name, fieldSchema] of Object.entries(shape)) fields[name] = extractFieldType(name, fieldSchema);
|
|
1812
|
+
return fields;
|
|
1813
|
+
}
|
|
1814
|
+
/**
|
|
1815
|
+
* Extract field type from Zod schema
|
|
1816
|
+
*/
|
|
1817
|
+
/** Type guard: check if a value is a non-null object */
|
|
1818
|
+
function isObject(value) {
|
|
1819
|
+
return typeof value === "object" && value !== null;
|
|
1820
|
+
}
|
|
1821
|
+
function extractFieldType(name, schema) {
|
|
1822
|
+
if (!isObject(schema)) return {
|
|
1823
|
+
kind: "string",
|
|
1824
|
+
label: formatLabel(name)
|
|
1825
|
+
};
|
|
1826
|
+
if (schema.isPortableText) return {
|
|
1827
|
+
kind: "portableText",
|
|
1828
|
+
label: formatLabel(name)
|
|
1829
|
+
};
|
|
1830
|
+
if (schema.isImage) return {
|
|
1831
|
+
kind: "image",
|
|
1832
|
+
label: formatLabel(name)
|
|
1833
|
+
};
|
|
1834
|
+
if (schema.isReference) return {
|
|
1835
|
+
kind: "reference",
|
|
1836
|
+
label: formatLabel(name)
|
|
1837
|
+
};
|
|
1838
|
+
const def = isObject(schema._def) ? schema._def : void 0;
|
|
1839
|
+
switch (typeof def?.typeName === "string" ? def.typeName : void 0) {
|
|
1840
|
+
case "ZodString": return {
|
|
1841
|
+
kind: "string",
|
|
1842
|
+
label: formatLabel(name)
|
|
1843
|
+
};
|
|
1844
|
+
case "ZodNumber": return {
|
|
1845
|
+
kind: "number",
|
|
1846
|
+
label: formatLabel(name)
|
|
1847
|
+
};
|
|
1848
|
+
case "ZodBoolean": return {
|
|
1849
|
+
kind: "boolean",
|
|
1850
|
+
label: formatLabel(name)
|
|
1851
|
+
};
|
|
1852
|
+
case "ZodDate": return {
|
|
1853
|
+
kind: "datetime",
|
|
1854
|
+
label: formatLabel(name)
|
|
1855
|
+
};
|
|
1856
|
+
case "ZodEnum": {
|
|
1857
|
+
const values = Array.isArray(def?.values) ? def.values : [];
|
|
1858
|
+
return {
|
|
1859
|
+
kind: "select",
|
|
1860
|
+
label: formatLabel(name),
|
|
1861
|
+
options: values.filter((v) => typeof v === "string").map((v) => ({
|
|
1862
|
+
value: v,
|
|
1863
|
+
label: v.charAt(0).toUpperCase() + v.slice(1)
|
|
1864
|
+
}))
|
|
1865
|
+
};
|
|
1866
|
+
}
|
|
1867
|
+
case "ZodArray": return {
|
|
1868
|
+
kind: "array",
|
|
1869
|
+
label: formatLabel(name)
|
|
1870
|
+
};
|
|
1871
|
+
case "ZodObject": return {
|
|
1872
|
+
kind: "object",
|
|
1873
|
+
label: formatLabel(name)
|
|
1874
|
+
};
|
|
1875
|
+
case "ZodOptional":
|
|
1876
|
+
case "ZodDefault":
|
|
1877
|
+
if (def?.innerType) return extractFieldType(name, def.innerType);
|
|
1878
|
+
return {
|
|
1879
|
+
kind: "string",
|
|
1880
|
+
label: formatLabel(name)
|
|
1881
|
+
};
|
|
1882
|
+
default: return {
|
|
1883
|
+
kind: "string",
|
|
1884
|
+
label: formatLabel(name)
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
function dbFieldDescriptor(field) {
|
|
1889
|
+
const entry = {
|
|
1890
|
+
kind: field.unsupportedType ? "unsupported" : FIELD_TYPE_TO_KIND[field.type],
|
|
1891
|
+
label: field.label,
|
|
1892
|
+
required: field.required,
|
|
1893
|
+
translatable: field.translatable,
|
|
1894
|
+
id: field.id
|
|
1895
|
+
};
|
|
1896
|
+
if (field.unsupportedType) entry.unsupportedType = field.unsupportedType;
|
|
1897
|
+
if (field.blockTypes) entry.blockTypes = field.blockTypes;
|
|
1898
|
+
if (field.blockTypeFingerprint) entry.blockTypeFingerprint = field.blockTypeFingerprint;
|
|
1899
|
+
if (field.widget) entry.widget = field.widget;
|
|
1900
|
+
if (field.options) entry.options = field.options;
|
|
1901
|
+
if (field.validation?.options) entry.options = field.validation.options.map((value) => ({
|
|
1902
|
+
value,
|
|
1903
|
+
label: value.charAt(0).toUpperCase() + value.slice(1)
|
|
1904
|
+
}));
|
|
1905
|
+
if (field.validation) entry.validation = { ...field.validation };
|
|
1906
|
+
return entry;
|
|
1907
|
+
}
|
|
1908
|
+
/**
|
|
1909
|
+
* Format field name as label
|
|
1910
|
+
*/
|
|
1911
|
+
function formatLabel(name) {
|
|
1912
|
+
return name.replace(CAMEL_CASE_PATTERN, " $1").replace(FIRST_CHAR_PATTERN, (str) => str.toUpperCase()).trim();
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
//#endregion
|
|
1916
|
+
//#region src/api/handlers/revision.ts
|
|
1917
|
+
/**
|
|
1918
|
+
* List revisions for a content entry
|
|
1919
|
+
*/
|
|
1920
|
+
async function handleRevisionList(db, collection, entryId, params = {}) {
|
|
1921
|
+
try {
|
|
1922
|
+
const repo = new RevisionRepository(db);
|
|
1923
|
+
const [items, total] = await Promise.all([repo.findByEntry(collection, entryId, { limit: Math.min(params.limit || 50, 100) }), repo.countByEntry(collection, entryId)]);
|
|
1924
|
+
return {
|
|
1925
|
+
success: true,
|
|
1926
|
+
data: {
|
|
1927
|
+
items,
|
|
1928
|
+
total
|
|
1929
|
+
}
|
|
1930
|
+
};
|
|
1931
|
+
} catch {
|
|
1932
|
+
return {
|
|
1933
|
+
success: false,
|
|
1934
|
+
error: {
|
|
1935
|
+
code: "REVISION_LIST_ERROR",
|
|
1936
|
+
message: "Failed to list revisions"
|
|
1937
|
+
}
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
/**
|
|
1942
|
+
* Get a specific revision
|
|
1943
|
+
*/
|
|
1944
|
+
async function handleRevisionGet(db, revisionId) {
|
|
1945
|
+
try {
|
|
1946
|
+
const item = await new RevisionRepository(db).findById(revisionId);
|
|
1947
|
+
if (!item) return {
|
|
1948
|
+
success: false,
|
|
1949
|
+
error: {
|
|
1950
|
+
code: "NOT_FOUND",
|
|
1951
|
+
message: `Revision not found: ${revisionId}`
|
|
1952
|
+
}
|
|
1953
|
+
};
|
|
1954
|
+
return {
|
|
1955
|
+
success: true,
|
|
1956
|
+
data: { item }
|
|
1957
|
+
};
|
|
1958
|
+
} catch {
|
|
1959
|
+
return {
|
|
1960
|
+
success: false,
|
|
1961
|
+
error: {
|
|
1962
|
+
code: "REVISION_GET_ERROR",
|
|
1963
|
+
message: "Failed to get revision"
|
|
1964
|
+
}
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
/**
|
|
1969
|
+
* Restore a revision (updates content to this revision's data and creates new revision)
|
|
1970
|
+
*/
|
|
1971
|
+
async function handleRevisionRestore(db, revisionId, callerUserId) {
|
|
1972
|
+
try {
|
|
1973
|
+
const revision = await new RevisionRepository(db).findById(revisionId);
|
|
1974
|
+
if (!revision) return {
|
|
1975
|
+
success: false,
|
|
1976
|
+
error: {
|
|
1977
|
+
code: "NOT_FOUND",
|
|
1978
|
+
message: `Revision not found: ${revisionId}`
|
|
1979
|
+
}
|
|
1980
|
+
};
|
|
1981
|
+
const { item, revisionId: queuedRevisionId } = await new ContentRepository(db).restoreRevision(revision.collection, revision.entryId, revision.data, callerUserId);
|
|
1982
|
+
const pruneRepo = new RevisionRepository(db);
|
|
1983
|
+
after(async () => {
|
|
1984
|
+
try {
|
|
1985
|
+
await pruneRepo.pruneQueuedEntry(revision.collection, revision.entryId, queuedRevisionId, 50);
|
|
1986
|
+
} catch (error) {
|
|
1987
|
+
console.error(`[revisions] Failed to prune revisions for ${revision.collection}/${revision.entryId}:`, error);
|
|
1988
|
+
}
|
|
1989
|
+
});
|
|
1990
|
+
return {
|
|
1991
|
+
success: true,
|
|
1992
|
+
data: {
|
|
1993
|
+
item,
|
|
1994
|
+
_rev: encodeRev(item)
|
|
1995
|
+
}
|
|
1996
|
+
};
|
|
1997
|
+
} catch (error) {
|
|
1998
|
+
if (error instanceof ContentMutationConflictError) return {
|
|
1999
|
+
success: false,
|
|
2000
|
+
error: {
|
|
2001
|
+
code: "CONFLICT",
|
|
2002
|
+
message: error.message
|
|
2003
|
+
}
|
|
2004
|
+
};
|
|
2005
|
+
return {
|
|
2006
|
+
success: false,
|
|
2007
|
+
error: {
|
|
2008
|
+
code: "REVISION_RESTORE_ERROR",
|
|
2009
|
+
message: "Failed to restore revision"
|
|
2010
|
+
}
|
|
2011
|
+
};
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
//#endregion
|
|
2016
|
+
//#region src/api/handlers/plugin-settings.ts
|
|
2017
|
+
function settingsKey(pluginId, key) {
|
|
2018
|
+
return `plugin:${pluginId}:settings:${key}`;
|
|
2019
|
+
}
|
|
2020
|
+
/**
|
|
2021
|
+
* Resolve a plugin's settings schema from either the configured
|
|
2022
|
+
* (in-process) plugin list or the statically-sandboxed entries.
|
|
2023
|
+
* Returns null when the plugin doesn't exist, undefined-equivalent
|
|
2024
|
+
* empty object when it declares no schema.
|
|
2025
|
+
*/
|
|
2026
|
+
function getPluginSettingsSchema(configuredPlugins, sandboxedPluginEntries, pluginId) {
|
|
2027
|
+
const plugin = configuredPlugins.find((p) => p.id === pluginId);
|
|
2028
|
+
if (plugin) return plugin.admin.settingsSchema ?? {};
|
|
2029
|
+
const sandboxed = sandboxedPluginEntries.find((e) => e.id === pluginId);
|
|
2030
|
+
if (sandboxed) return sandboxed.settingsSchema ?? {};
|
|
2031
|
+
return null;
|
|
2032
|
+
}
|
|
2033
|
+
/**
|
|
2034
|
+
* Validate a single value against its schema field.
|
|
2035
|
+
* Returns an error message, or null when valid.
|
|
2036
|
+
*/
|
|
2037
|
+
function validateValue(key, field, value) {
|
|
2038
|
+
switch (field.type) {
|
|
2039
|
+
case "string":
|
|
2040
|
+
case "secret":
|
|
2041
|
+
case "url":
|
|
2042
|
+
case "email":
|
|
2043
|
+
if (typeof value !== "string") return `Setting "${key}" must be a string`;
|
|
2044
|
+
if (field.type === "url" && value !== "" && !URL.canParse(value)) return `Setting "${key}" must be a valid URL`;
|
|
2045
|
+
if (field.type === "email" && value !== "" && !value.includes("@")) return `Setting "${key}" must be a valid email address`;
|
|
2046
|
+
return null;
|
|
2047
|
+
case "number":
|
|
2048
|
+
if (typeof value !== "number" || Number.isNaN(value)) return `Setting "${key}" must be a number`;
|
|
2049
|
+
if (field.min !== void 0 && value < field.min) return `Setting "${key}" must be at least ${field.min}`;
|
|
2050
|
+
if (field.max !== void 0 && value > field.max) return `Setting "${key}" must be at most ${field.max}`;
|
|
2051
|
+
return null;
|
|
2052
|
+
case "boolean": return typeof value === "boolean" ? null : `Setting "${key}" must be a boolean`;
|
|
2053
|
+
case "select":
|
|
2054
|
+
if (typeof value !== "string" || !field.options.some((o) => o.value === value)) return `Setting "${key}" must be one of the defined options`;
|
|
2055
|
+
return null;
|
|
2056
|
+
default: return `Setting "${key}" has an unknown field type`;
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
async function buildSettingsResponse(optionsRepo, pluginId, schema) {
|
|
2060
|
+
const keys = Object.keys(schema);
|
|
2061
|
+
const stored = await optionsRepo.getMany(keys.map((key) => settingsKey(pluginId, key)));
|
|
2062
|
+
const values = {};
|
|
2063
|
+
const secretsSet = {};
|
|
2064
|
+
for (const key of keys) {
|
|
2065
|
+
const field = schema[key];
|
|
2066
|
+
if (!field) continue;
|
|
2067
|
+
const storedValue = stored.get(settingsKey(pluginId, key));
|
|
2068
|
+
if (field.type === "secret") {
|
|
2069
|
+
secretsSet[key] = storedValue !== void 0 && storedValue !== null && storedValue !== "";
|
|
2070
|
+
continue;
|
|
2071
|
+
}
|
|
2072
|
+
if (storedValue !== void 0 && storedValue !== null) values[key] = storedValue;
|
|
2073
|
+
else if ("default" in field && field.default !== void 0) values[key] = field.default;
|
|
2074
|
+
else values[key] = null;
|
|
2075
|
+
}
|
|
2076
|
+
return {
|
|
2077
|
+
schema,
|
|
2078
|
+
values,
|
|
2079
|
+
secretsSet
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
/**
|
|
2083
|
+
* Get a plugin's settings (schema + current values, secrets masked)
|
|
2084
|
+
*/
|
|
2085
|
+
async function handlePluginSettingsGet(db, pluginId, schema) {
|
|
2086
|
+
try {
|
|
2087
|
+
return {
|
|
2088
|
+
success: true,
|
|
2089
|
+
data: await buildSettingsResponse(new OptionsRepository(db), pluginId, schema)
|
|
2090
|
+
};
|
|
2091
|
+
} catch {
|
|
2092
|
+
return {
|
|
2093
|
+
success: false,
|
|
2094
|
+
error: {
|
|
2095
|
+
code: ErrorCode.PLUGIN_SETTINGS_READ_ERROR,
|
|
2096
|
+
message: "Failed to read plugin settings"
|
|
2097
|
+
}
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
/**
|
|
2102
|
+
* Update a plugin's settings.
|
|
2103
|
+
*
|
|
2104
|
+
* Only keys present in `updates` are written. A `null` value deletes the
|
|
2105
|
+
* stored value (reverting to the schema default). Secret fields are
|
|
2106
|
+
* write-only: they accept a new string value or `null` to clear, and the
|
|
2107
|
+
* response never echoes them back.
|
|
2108
|
+
*/
|
|
2109
|
+
async function handlePluginSettingsUpdate(db, pluginId, schema, updates) {
|
|
2110
|
+
try {
|
|
2111
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
2112
|
+
const field = schema[key];
|
|
2113
|
+
if (!field) return {
|
|
2114
|
+
success: false,
|
|
2115
|
+
error: {
|
|
2116
|
+
code: ErrorCode.VALIDATION_ERROR,
|
|
2117
|
+
message: `Unknown setting "${key}" for plugin "${pluginId}"`
|
|
2118
|
+
}
|
|
2119
|
+
};
|
|
2120
|
+
if (value === null) continue;
|
|
2121
|
+
const error = validateValue(key, field, value);
|
|
2122
|
+
if (error) return {
|
|
2123
|
+
success: false,
|
|
2124
|
+
error: {
|
|
2125
|
+
code: ErrorCode.VALIDATION_ERROR,
|
|
2126
|
+
message: error
|
|
2127
|
+
}
|
|
2128
|
+
};
|
|
2129
|
+
}
|
|
2130
|
+
const encodedUpdates = /* @__PURE__ */ new Map();
|
|
2131
|
+
for (const [key, value] of Object.entries(updates)) encodedUpdates.set(key, value === null ? null : await encodePluginSettingValue(pluginId, key, value, schema));
|
|
2132
|
+
return {
|
|
2133
|
+
success: true,
|
|
2134
|
+
data: await withTransaction(db, async (trx) => {
|
|
2135
|
+
const txRepo = new OptionsRepository(trx);
|
|
2136
|
+
for (const [key, value] of encodedUpdates) {
|
|
2137
|
+
if (value === null) await txRepo.delete(settingsKey(pluginId, key));
|
|
2138
|
+
else await txRepo.set(settingsKey(pluginId, key), value);
|
|
2139
|
+
await trx.deleteFrom("_plugin_storage").where("plugin_id", "=", pluginId).where("collection", "=", "__kv").where("id", "=", `settings:${key}`).execute();
|
|
2140
|
+
}
|
|
2141
|
+
return buildSettingsResponse(txRepo, pluginId, schema);
|
|
2142
|
+
})
|
|
2143
|
+
};
|
|
2144
|
+
} catch (error) {
|
|
2145
|
+
if (error instanceof PluginSettingEncryptionError) return {
|
|
2146
|
+
success: false,
|
|
2147
|
+
error: {
|
|
2148
|
+
code: error.code,
|
|
2149
|
+
message: error.message
|
|
2150
|
+
}
|
|
2151
|
+
};
|
|
2152
|
+
if (error instanceof EmDashSecretsError) return {
|
|
2153
|
+
success: false,
|
|
2154
|
+
error: {
|
|
2155
|
+
code: ErrorCode.PLUGIN_SETTING_ENCRYPTION_KEY_INVALID,
|
|
2156
|
+
message: "EMDASH_ENCRYPTION_KEY is malformed. Restore or correct the configured key from its secret backup. Generate a new key only if no stored plugin setting depends on the lost key."
|
|
2157
|
+
}
|
|
2158
|
+
};
|
|
2159
|
+
return {
|
|
2160
|
+
success: false,
|
|
2161
|
+
error: {
|
|
2162
|
+
code: ErrorCode.PLUGIN_SETTINGS_UPDATE_ERROR,
|
|
2163
|
+
message: "Failed to update plugin settings"
|
|
2164
|
+
}
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
//#endregion
|
|
2170
|
+
//#region src/plugins/marketplace.ts
|
|
2171
|
+
/**
|
|
2172
|
+
* MarketplaceClient — HTTP client for the EmDash Plugin Marketplace
|
|
2173
|
+
*
|
|
2174
|
+
* Used by the install/update/proxy endpoints in EmDash core to communicate
|
|
2175
|
+
* with the marketplace Worker. The marketplace is a distribution channel,
|
|
2176
|
+
* not a runtime dependency — bundles are copied to site-local R2 at install time.
|
|
2177
|
+
*/
|
|
2178
|
+
const TRAILING_SLASHES = /\/+$/;
|
|
2179
|
+
const LEADING_DOT_SLASH = /^\.\//;
|
|
2180
|
+
var MarketplaceError = class extends Error {
|
|
2181
|
+
constructor(message, status, code) {
|
|
2182
|
+
super(message);
|
|
2183
|
+
this.status = status;
|
|
2184
|
+
this.code = code;
|
|
2185
|
+
this.name = "MarketplaceError";
|
|
2186
|
+
}
|
|
2187
|
+
};
|
|
2188
|
+
var MarketplaceUnavailableError = class extends MarketplaceError {
|
|
2189
|
+
constructor(cause) {
|
|
2190
|
+
super("Plugin marketplace is unavailable", void 0, "MARKETPLACE_UNAVAILABLE");
|
|
2191
|
+
if (cause) this.cause = cause;
|
|
2192
|
+
}
|
|
2193
|
+
};
|
|
2194
|
+
var MarketplaceClientImpl = class {
|
|
2195
|
+
baseUrl;
|
|
2196
|
+
siteOrigin;
|
|
2197
|
+
constructor(baseUrl, siteOrigin) {
|
|
2198
|
+
this.baseUrl = baseUrl.replace(TRAILING_SLASHES, "");
|
|
2199
|
+
this.siteOrigin = siteOrigin;
|
|
2200
|
+
}
|
|
2201
|
+
async search(query, opts) {
|
|
2202
|
+
const params = new URLSearchParams();
|
|
2203
|
+
if (query) params.set("q", query);
|
|
2204
|
+
if (opts?.category) params.set("category", opts.category);
|
|
2205
|
+
if (opts?.capability) params.set("capability", opts.capability);
|
|
2206
|
+
if (opts?.sort) params.set("sort", opts.sort);
|
|
2207
|
+
if (opts?.cursor) params.set("cursor", opts.cursor);
|
|
2208
|
+
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
2209
|
+
const qs = params.toString();
|
|
2210
|
+
const url = `${this.baseUrl}/api/v1/plugins${qs ? `?${qs}` : ""}`;
|
|
2211
|
+
return await this.fetchJson(url);
|
|
2212
|
+
}
|
|
2213
|
+
async getPlugin(id) {
|
|
2214
|
+
const url = `${this.baseUrl}/api/v1/plugins/${encodeURIComponent(id)}`;
|
|
2215
|
+
return this.fetchJson(url);
|
|
2216
|
+
}
|
|
2217
|
+
async getVersions(id) {
|
|
2218
|
+
const url = `${this.baseUrl}/api/v1/plugins/${encodeURIComponent(id)}/versions`;
|
|
2219
|
+
return (await this.fetchJson(url)).items;
|
|
2220
|
+
}
|
|
2221
|
+
async downloadBundle(id, version) {
|
|
2222
|
+
const bundleUrl = `${this.baseUrl}/api/v1/plugins/${encodeURIComponent(id)}/versions/${encodeURIComponent(version)}/bundle`;
|
|
2223
|
+
const marketplaceOrigin = new URL(this.baseUrl).origin;
|
|
2224
|
+
const MAX_REDIRECTS = 5;
|
|
2225
|
+
let response;
|
|
2226
|
+
try {
|
|
2227
|
+
let currentUrl = bundleUrl;
|
|
2228
|
+
response = await fetch(currentUrl, { redirect: "manual" });
|
|
2229
|
+
for (let i = 0; i < MAX_REDIRECTS; i++) {
|
|
2230
|
+
if (response.status < 300 || response.status >= 400) break;
|
|
2231
|
+
const location = response.headers.get("location");
|
|
2232
|
+
if (!location) break;
|
|
2233
|
+
const target = new URL(location, currentUrl);
|
|
2234
|
+
if (target.origin !== marketplaceOrigin) throw new MarketplaceError(`Bundle download redirected to untrusted host: ${target.origin}`, response.status, "BUNDLE_REDIRECT_UNTRUSTED");
|
|
2235
|
+
currentUrl = target.href;
|
|
2236
|
+
response = await fetch(currentUrl, { redirect: "manual" });
|
|
2237
|
+
}
|
|
2238
|
+
if (response.status >= 300 && response.status < 400) throw new MarketplaceError(`Bundle download exceeded maximum redirects (${MAX_REDIRECTS})`, response.status, "BUNDLE_TOO_MANY_REDIRECTS");
|
|
2239
|
+
} catch (err) {
|
|
2240
|
+
if (err instanceof MarketplaceError) throw err;
|
|
2241
|
+
throw new MarketplaceUnavailableError(err);
|
|
2242
|
+
}
|
|
2243
|
+
if (!response.ok) throw new MarketplaceError(`Failed to download bundle: ${response.status} ${response.statusText}`, response.status, "BUNDLE_DOWNLOAD_FAILED");
|
|
2244
|
+
const tarballBytes = new Uint8Array(await response.arrayBuffer());
|
|
2245
|
+
try {
|
|
2246
|
+
return await extractBundle(tarballBytes);
|
|
2247
|
+
} catch (err) {
|
|
2248
|
+
if (err instanceof MarketplaceError) throw err;
|
|
2249
|
+
throw new MarketplaceError("Failed to extract plugin bundle", void 0, "BUNDLE_EXTRACT_FAILED");
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
async reportInstall(id, version) {
|
|
2253
|
+
const siteHash = await generateSiteHash(this.siteOrigin);
|
|
2254
|
+
const url = `${this.baseUrl}/api/v1/plugins/${encodeURIComponent(id)}/installs`;
|
|
2255
|
+
try {
|
|
2256
|
+
await fetch(url, {
|
|
2257
|
+
method: "POST",
|
|
2258
|
+
headers: { "Content-Type": "application/json" },
|
|
2259
|
+
body: JSON.stringify({
|
|
2260
|
+
siteHash,
|
|
2261
|
+
version
|
|
2262
|
+
})
|
|
2263
|
+
});
|
|
2264
|
+
} catch {}
|
|
2265
|
+
}
|
|
2266
|
+
async searchThemes(query, opts) {
|
|
2267
|
+
const params = new URLSearchParams();
|
|
2268
|
+
if (query) params.set("q", query);
|
|
2269
|
+
if (opts?.keyword) params.set("keyword", opts.keyword);
|
|
2270
|
+
if (opts?.sort) params.set("sort", opts.sort);
|
|
2271
|
+
if (opts?.cursor) params.set("cursor", opts.cursor);
|
|
2272
|
+
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
2273
|
+
const qs = params.toString();
|
|
2274
|
+
const url = `${this.baseUrl}/api/v1/themes${qs ? `?${qs}` : ""}`;
|
|
2275
|
+
return this.fetchJson(url);
|
|
2276
|
+
}
|
|
2277
|
+
async getTheme(id) {
|
|
2278
|
+
const url = `${this.baseUrl}/api/v1/themes/${encodeURIComponent(id)}`;
|
|
2279
|
+
return this.fetchJson(url);
|
|
2280
|
+
}
|
|
2281
|
+
async fetchJson(url) {
|
|
2282
|
+
let response;
|
|
2283
|
+
try {
|
|
2284
|
+
response = await fetch(url, { headers: { Accept: "application/json" } });
|
|
2285
|
+
} catch (err) {
|
|
2286
|
+
throw new MarketplaceUnavailableError(err);
|
|
2287
|
+
}
|
|
2288
|
+
if (!response.ok) {
|
|
2289
|
+
let errorMessage = `Marketplace request failed: ${response.status}`;
|
|
2290
|
+
try {
|
|
2291
|
+
const body = await response.json();
|
|
2292
|
+
if (body.error) errorMessage = body.error;
|
|
2293
|
+
} catch {}
|
|
2294
|
+
throw new MarketplaceError(errorMessage, response.status);
|
|
2295
|
+
}
|
|
2296
|
+
return await response.json();
|
|
2297
|
+
}
|
|
2298
|
+
};
|
|
2299
|
+
/**
|
|
2300
|
+
* Extract manifest + code files from a tarball.
|
|
2301
|
+
*
|
|
2302
|
+
* The tarball is a gzipped tar archive containing:
|
|
2303
|
+
* - manifest.json
|
|
2304
|
+
* - backend.js
|
|
2305
|
+
* - admin.js (optional)
|
|
2306
|
+
*
|
|
2307
|
+
* We use a minimal tar parser since we only need to read a few small files.
|
|
2308
|
+
*/
|
|
2309
|
+
/**
|
|
2310
|
+
* Exported so the experimental registry install handler can reuse the
|
|
2311
|
+
* same parse / validate / hash primitive. Despite the file name, this
|
|
2312
|
+
* function predates the marketplace-vs-registry split and is generic
|
|
2313
|
+
* over plugin bundle tarballs regardless of distribution channel.
|
|
2314
|
+
*/
|
|
2315
|
+
const MAX_DECOMPRESSED_BUNDLE_BYTES = 256 * 1024;
|
|
2316
|
+
const MAX_BUNDLE_TAR_ENTRIES = 32;
|
|
2317
|
+
async function extractBundle(tarballBytes) {
|
|
2318
|
+
const reader = new ReadableStream({ start(controller) {
|
|
2319
|
+
controller.enqueue(tarballBytes);
|
|
2320
|
+
controller.close();
|
|
2321
|
+
} }).pipeThrough(createGzipDecoder()).getReader();
|
|
2322
|
+
const chunks = [];
|
|
2323
|
+
let total = 0;
|
|
2324
|
+
while (true) {
|
|
2325
|
+
const { done, value } = await reader.read();
|
|
2326
|
+
if (done) break;
|
|
2327
|
+
if (!value) continue;
|
|
2328
|
+
total += value.byteLength;
|
|
2329
|
+
if (total > MAX_DECOMPRESSED_BUNDLE_BYTES) {
|
|
2330
|
+
try {
|
|
2331
|
+
await reader.cancel();
|
|
2332
|
+
} catch {}
|
|
2333
|
+
throw new MarketplaceError(`Bundle decompressed size exceeds limit (${MAX_DECOMPRESSED_BUNDLE_BYTES} bytes)`, void 0, "INVALID_BUNDLE");
|
|
2334
|
+
}
|
|
2335
|
+
chunks.push(value);
|
|
2336
|
+
}
|
|
2337
|
+
const decompressedBytes = new Uint8Array(total);
|
|
2338
|
+
{
|
|
2339
|
+
let offset = 0;
|
|
2340
|
+
for (const chunk of chunks) {
|
|
2341
|
+
decompressedBytes.set(chunk, offset);
|
|
2342
|
+
offset += chunk.byteLength;
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
const entries = await unpackTar(new ReadableStream({ start(controller) {
|
|
2346
|
+
controller.enqueue(decompressedBytes);
|
|
2347
|
+
controller.close();
|
|
2348
|
+
} }));
|
|
2349
|
+
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");
|
|
2350
|
+
const decoder = new TextDecoder();
|
|
2351
|
+
const files = /* @__PURE__ */ new Map();
|
|
2352
|
+
for (const entry of entries) if (entry.data && entry.header.type === "file") {
|
|
2353
|
+
const name = entry.header.name.replace(LEADING_DOT_SLASH, "");
|
|
2354
|
+
files.set(name, decoder.decode(entry.data));
|
|
2355
|
+
}
|
|
2356
|
+
const manifestJson = files.get("manifest.json");
|
|
2357
|
+
const backendCode = files.get("backend.js");
|
|
2358
|
+
if (!manifestJson) throw new MarketplaceError("Invalid bundle: missing manifest.json", void 0, "INVALID_BUNDLE");
|
|
2359
|
+
if (!backendCode) throw new MarketplaceError("Invalid bundle: missing backend.js", void 0, "INVALID_BUNDLE");
|
|
2360
|
+
let manifest;
|
|
2361
|
+
try {
|
|
2362
|
+
const parsed = JSON.parse(manifestJson);
|
|
2363
|
+
const result = pluginManifestSchema$1.safeParse(parsed);
|
|
2364
|
+
if (!result.success) throw new MarketplaceError("Invalid bundle: manifest.json failed validation", void 0, "INVALID_BUNDLE");
|
|
2365
|
+
manifest = reconcileManifestAccess$1(result.data);
|
|
2366
|
+
} catch (err) {
|
|
2367
|
+
if (err instanceof MarketplaceError) throw err;
|
|
2368
|
+
throw new MarketplaceError("Invalid bundle: malformed manifest.json", void 0, "INVALID_BUNDLE");
|
|
2369
|
+
}
|
|
2370
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", tarballBytes);
|
|
2371
|
+
const hashArray = new Uint8Array(hashBuffer);
|
|
2372
|
+
const checksum = Array.from(hashArray, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
2373
|
+
return {
|
|
2374
|
+
manifest,
|
|
2375
|
+
backendCode,
|
|
2376
|
+
adminCode: files.get("admin.js"),
|
|
2377
|
+
checksum
|
|
2378
|
+
};
|
|
2379
|
+
}
|
|
2380
|
+
/**
|
|
2381
|
+
* Generate a stable non-identifying site hash from the site origin.
|
|
2382
|
+
* The same origin always produces the same hash, so the marketplace
|
|
2383
|
+
* installs table deduplicates correctly per (plugin_id, site_hash).
|
|
2384
|
+
*/
|
|
2385
|
+
async function generateSiteHash(siteOrigin) {
|
|
2386
|
+
const seed = siteOrigin ? `emdash-site:${siteOrigin}` : `emdash-anonymous`;
|
|
2387
|
+
try {
|
|
2388
|
+
const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(seed));
|
|
2389
|
+
const arr = new Uint8Array(hash);
|
|
2390
|
+
return Array.from(arr.slice(0, 8), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
2391
|
+
} catch {
|
|
2392
|
+
let h = 2166136261;
|
|
2393
|
+
for (let i = 0; i < seed.length; i++) {
|
|
2394
|
+
h ^= seed.charCodeAt(i);
|
|
2395
|
+
h = Math.imul(h, 16777619);
|
|
2396
|
+
}
|
|
2397
|
+
const h2 = h ^ h >>> 16;
|
|
2398
|
+
return (h >>> 0).toString(16).padStart(8, "0") + (h2 >>> 0).toString(16).padStart(8, "0");
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
/**
|
|
2402
|
+
* Create a MarketplaceClient for the given marketplace URL.
|
|
2403
|
+
*
|
|
2404
|
+
* @param baseUrl - The marketplace API base URL (e.g. "https://marketplace.emdashcms.com")
|
|
2405
|
+
* @param siteOrigin - The origin of the EmDash site (e.g. "https://myblog.example.com").
|
|
2406
|
+
* Used to generate a stable, non-identifying site hash for install deduplication.
|
|
2407
|
+
*/
|
|
2408
|
+
function createMarketplaceClient(baseUrl, siteOrigin) {
|
|
2409
|
+
return new MarketplaceClientImpl(baseUrl, siteOrigin);
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
//#endregion
|
|
2413
|
+
//#region src/plugins/storage-indexes.ts
|
|
2414
|
+
/**
|
|
2415
|
+
* Generate a deterministic index name.
|
|
2416
|
+
* Unique indexes use a `uidx_` prefix to avoid collisions with regular indexes on the same fields.
|
|
2417
|
+
*/
|
|
2418
|
+
function generateIndexName(pluginId, collection, fields, options) {
|
|
2419
|
+
return `${options?.unique ? "uidx" : "idx"}_plugin_${pluginId}_${collection}_${fields.join("_")}`.substring(0, 128);
|
|
2420
|
+
}
|
|
2421
|
+
/**
|
|
2422
|
+
* Generate a Kysely sql expression for creating an expression index.
|
|
2423
|
+
*
|
|
2424
|
+
* Validates all inputs before interpolation. The collection uses the
|
|
2425
|
+
* permissive manifest-key rules rather than SQL-identifier rules — it is
|
|
2426
|
+
* stored as opaque text and only reaches SQL inside the generated index
|
|
2427
|
+
* name — so kebab-case collections index like any other.
|
|
2428
|
+
*/
|
|
2429
|
+
function generateCreateIndexSql(db, pluginId, collection, fields, options) {
|
|
2430
|
+
validatePluginIdentifier(pluginId, "plugin ID");
|
|
2431
|
+
validateStorageCollectionName(collection, "collection name");
|
|
2432
|
+
for (const field of fields) validateJsonFieldName(field, "index field name");
|
|
2433
|
+
const indexName = generateIndexName(pluginId, collection, fields, options);
|
|
2434
|
+
const expressions = fields.map((field) => {
|
|
2435
|
+
if (isPostgres(db)) return `(${jsonExtractExpr(db, "data", field)})`;
|
|
2436
|
+
return jsonExtractExpr(db, "data", field);
|
|
2437
|
+
}).join(", ");
|
|
2438
|
+
const createKeyword = options?.unique ? "CREATE UNIQUE INDEX" : "CREATE INDEX";
|
|
2439
|
+
return sql`${sql.raw(createKeyword)} IF NOT EXISTS ${sql.ref(indexName)}
|
|
2440
|
+
ON _plugin_storage(plugin_id, collection, ${sql.raw(expressions)})
|
|
2441
|
+
`;
|
|
2442
|
+
}
|
|
2443
|
+
/**
|
|
2444
|
+
* Generate a Kysely sql expression for dropping an index.
|
|
2445
|
+
*
|
|
2446
|
+
* Uses sql.ref() for safe identifier quoting.
|
|
2447
|
+
*/
|
|
2448
|
+
function generateDropIndexSql(indexName) {
|
|
2449
|
+
return sql`DROP INDEX IF EXISTS ${sql.ref(indexName)}`;
|
|
2450
|
+
}
|
|
2451
|
+
/**
|
|
2452
|
+
* Normalize index declarations to arrays of field arrays
|
|
2453
|
+
*/
|
|
2454
|
+
function normalizeIndexes(indexes) {
|
|
2455
|
+
return indexes.map((index) => Array.isArray(index) ? index : [index]);
|
|
2456
|
+
}
|
|
2457
|
+
/**
|
|
2458
|
+
* Create all declared indexes for a plugin collection
|
|
2459
|
+
*/
|
|
2460
|
+
async function createStorageIndexes(db, pluginId, collection, indexes, options) {
|
|
2461
|
+
const normalized = normalizeIndexes(indexes);
|
|
2462
|
+
const uniqueNormalized = options?.uniqueIndexes ? normalizeIndexes(options.uniqueIndexes) : [];
|
|
2463
|
+
const uniqueSet = new Set(uniqueNormalized.map((f) => f.join(",")));
|
|
2464
|
+
const allEntries = [...normalized.filter((f) => !uniqueSet.has(f.join(","))).map((fields) => ({
|
|
2465
|
+
fields,
|
|
2466
|
+
unique: false
|
|
2467
|
+
})), ...uniqueNormalized.map((fields) => ({
|
|
2468
|
+
fields,
|
|
2469
|
+
unique: true
|
|
2470
|
+
}))];
|
|
2471
|
+
const created = [];
|
|
2472
|
+
const errors = [];
|
|
2473
|
+
for (const entry of allEntries) {
|
|
2474
|
+
const { fields } = entry;
|
|
2475
|
+
const indexName = generateIndexName(pluginId, collection, fields, { unique: entry.unique });
|
|
2476
|
+
try {
|
|
2477
|
+
await generateCreateIndexSql(db, pluginId, collection, fields, { unique: entry.unique }).execute(db);
|
|
2478
|
+
await db.insertInto("_plugin_indexes").values({
|
|
2479
|
+
plugin_id: pluginId,
|
|
2480
|
+
collection,
|
|
2481
|
+
index_name: indexName,
|
|
2482
|
+
fields: JSON.stringify(fields)
|
|
2483
|
+
}).onConflict((oc) => oc.columns([
|
|
2484
|
+
"plugin_id",
|
|
2485
|
+
"collection",
|
|
2486
|
+
"index_name"
|
|
2487
|
+
]).doUpdateSet({ fields: JSON.stringify(fields) })).execute();
|
|
2488
|
+
created.push(indexName);
|
|
2489
|
+
} catch (error) {
|
|
2490
|
+
errors.push({
|
|
2491
|
+
index: indexName,
|
|
2492
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2493
|
+
});
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
return {
|
|
2497
|
+
created,
|
|
2498
|
+
errors
|
|
2499
|
+
};
|
|
2500
|
+
}
|
|
2501
|
+
/**
|
|
2502
|
+
* Remove indexes that are no longer declared
|
|
2503
|
+
*/
|
|
2504
|
+
async function removeOrphanedIndexes(db, pluginId, collection, currentIndexes, options) {
|
|
2505
|
+
const normalized = normalizeIndexes(currentIndexes);
|
|
2506
|
+
const uniqueNormalized = options?.uniqueIndexes ? normalizeIndexes(options.uniqueIndexes) : [];
|
|
2507
|
+
const uniqueSet = new Set(uniqueNormalized.map((f) => f.join(",")));
|
|
2508
|
+
const currentIndexNames = /* @__PURE__ */ new Set();
|
|
2509
|
+
for (const fields of normalized) if (!uniqueSet.has(fields.join(","))) currentIndexNames.add(generateIndexName(pluginId, collection, fields));
|
|
2510
|
+
for (const fields of uniqueNormalized) currentIndexNames.add(generateIndexName(pluginId, collection, fields, { unique: true }));
|
|
2511
|
+
const existingIndexes = await db.selectFrom("_plugin_indexes").select(["index_name"]).where("plugin_id", "=", pluginId).where("collection", "=", collection).execute();
|
|
2512
|
+
const removed = [];
|
|
2513
|
+
const errors = [];
|
|
2514
|
+
for (const { index_name } of existingIndexes) if (!currentIndexNames.has(index_name)) try {
|
|
2515
|
+
await generateDropIndexSql(index_name).execute(db);
|
|
2516
|
+
await db.deleteFrom("_plugin_indexes").where("plugin_id", "=", pluginId).where("collection", "=", collection).where("index_name", "=", index_name).execute();
|
|
2517
|
+
removed.push(index_name);
|
|
2518
|
+
} catch (error) {
|
|
2519
|
+
errors.push({
|
|
2520
|
+
index: index_name,
|
|
2521
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2522
|
+
});
|
|
2523
|
+
}
|
|
2524
|
+
return {
|
|
2525
|
+
removed,
|
|
2526
|
+
errors
|
|
2527
|
+
};
|
|
2528
|
+
}
|
|
2529
|
+
/**
|
|
2530
|
+
* Sync indexes for a plugin collection (create new, remove old)
|
|
2531
|
+
*/
|
|
2532
|
+
async function syncStorageIndexes(db, pluginId, collection, indexes, options) {
|
|
2533
|
+
const [createResult, removeResult] = await Promise.all([createStorageIndexes(db, pluginId, collection, indexes, options), removeOrphanedIndexes(db, pluginId, collection, indexes, options)]);
|
|
2534
|
+
return {
|
|
2535
|
+
created: createResult.created,
|
|
2536
|
+
removed: removeResult.removed,
|
|
2537
|
+
errors: [...createResult.errors, ...removeResult.errors]
|
|
2538
|
+
};
|
|
2539
|
+
}
|
|
2540
|
+
/**
|
|
2541
|
+
* Materialize the storage indexes a set of plugins declare in their
|
|
2542
|
+
* manifests. Failures are logged per collection and never thrown — a missing
|
|
2543
|
+
* index affects query performance, not correctness, so it must not fail an
|
|
2544
|
+
* install or a scheduler tick.
|
|
2545
|
+
*/
|
|
2546
|
+
async function syncDeclaredStorageIndexes(db, plugins) {
|
|
2547
|
+
for (const plugin of plugins) for (const [collection, config] of Object.entries(plugin.storage ?? {})) try {
|
|
2548
|
+
const result = await syncStorageIndexes(db, plugin.id, collection, config.indexes, { uniqueIndexes: config.uniqueIndexes });
|
|
2549
|
+
for (const failure of result.errors) console.error(`[plugins] Failed to sync storage index ${failure.index} for ${plugin.id}/${collection}: ${failure.error}`);
|
|
2550
|
+
} catch (error) {
|
|
2551
|
+
console.error(`[plugins] Failed to sync storage indexes for ${plugin.id}/${collection}:`, error);
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
/**
|
|
2555
|
+
* Remove all indexes for a plugin
|
|
2556
|
+
*/
|
|
2557
|
+
async function removeAllPluginIndexes(db, pluginId) {
|
|
2558
|
+
const existingIndexes = await db.selectFrom("_plugin_indexes").select(["index_name", "collection"]).where("plugin_id", "=", pluginId).execute();
|
|
2559
|
+
const removed = [];
|
|
2560
|
+
const errors = [];
|
|
2561
|
+
for (const { index_name } of existingIndexes) try {
|
|
2562
|
+
await generateDropIndexSql(index_name).execute(db);
|
|
2563
|
+
removed.push(index_name);
|
|
2564
|
+
} catch (error) {
|
|
2565
|
+
errors.push({
|
|
2566
|
+
index: index_name,
|
|
2567
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2568
|
+
});
|
|
2569
|
+
}
|
|
2570
|
+
await db.deleteFrom("_plugin_indexes").where("plugin_id", "=", pluginId).execute();
|
|
2571
|
+
return {
|
|
2572
|
+
removed,
|
|
2573
|
+
errors
|
|
2574
|
+
};
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
//#endregion
|
|
2578
|
+
//#region src/api/handlers/marketplace.ts
|
|
2579
|
+
/** Semver-like pattern: digits, dots, hyphens, plus signs (e.g. 1.0.0, 1.0.0-beta.1) */
|
|
2580
|
+
const VERSION_PATTERN = /^[a-z0-9][a-z0-9._+-]*$/i;
|
|
2581
|
+
function validateVersion(version) {
|
|
2582
|
+
if (version.includes("..")) throw new Error("Invalid version format");
|
|
2583
|
+
if (!VERSION_PATTERN.test(version)) throw new Error("Invalid version format");
|
|
2584
|
+
}
|
|
2585
|
+
function getClient(marketplaceUrl, siteOrigin) {
|
|
2586
|
+
if (!marketplaceUrl) return null;
|
|
2587
|
+
return createMarketplaceClient(marketplaceUrl, siteOrigin);
|
|
2588
|
+
}
|
|
2589
|
+
function diffCapabilities(oldCaps, newCaps) {
|
|
2590
|
+
const oldNorm = normalizeCapabilities(oldCaps);
|
|
2591
|
+
const newNorm = normalizeCapabilities(newCaps);
|
|
2592
|
+
const oldSet = new Set(oldNorm);
|
|
2593
|
+
const newSet = new Set(newNorm);
|
|
2594
|
+
return {
|
|
2595
|
+
added: newNorm.filter((c) => !oldSet.has(c)),
|
|
2596
|
+
removed: oldNorm.filter((c) => !newSet.has(c))
|
|
2597
|
+
};
|
|
2598
|
+
}
|
|
2599
|
+
/**
|
|
2600
|
+
* Diff route visibility between two manifests.
|
|
2601
|
+
* Returns routes that changed from private to public (newly exposed).
|
|
2602
|
+
*/
|
|
2603
|
+
function diffRouteVisibility(oldManifest, newManifest) {
|
|
2604
|
+
const oldPublicRoutes = /* @__PURE__ */ new Set();
|
|
2605
|
+
if (oldManifest) for (const entry of oldManifest.routes) {
|
|
2606
|
+
const normalized = normalizeManifestRoute$1(entry);
|
|
2607
|
+
if (normalized.public === true) oldPublicRoutes.add(normalized.name);
|
|
2608
|
+
}
|
|
2609
|
+
const newlyPublic = [];
|
|
2610
|
+
for (const entry of newManifest.routes) {
|
|
2611
|
+
const normalized = normalizeManifestRoute$1(entry);
|
|
2612
|
+
if (normalized.public === true && !oldPublicRoutes.has(normalized.name)) newlyPublic.push(normalized.name);
|
|
2613
|
+
}
|
|
2614
|
+
return { newlyPublic };
|
|
2615
|
+
}
|
|
2616
|
+
async function resolveVersionMetadata(client, pluginId, pluginDetail, version) {
|
|
2617
|
+
if (pluginDetail.latestVersion?.version === version) return {
|
|
2618
|
+
version: pluginDetail.latestVersion.version,
|
|
2619
|
+
minEmDashVersion: pluginDetail.latestVersion.minEmDashVersion,
|
|
2620
|
+
bundleSize: pluginDetail.latestVersion.bundleSize,
|
|
2621
|
+
checksum: pluginDetail.latestVersion.checksum,
|
|
2622
|
+
changelog: pluginDetail.latestVersion.changelog,
|
|
2623
|
+
capabilities: pluginDetail.latestVersion.capabilities,
|
|
2624
|
+
status: pluginDetail.latestVersion.status,
|
|
2625
|
+
auditVerdict: pluginDetail.latestVersion.audit?.verdict ?? null,
|
|
2626
|
+
imageAuditVerdict: pluginDetail.latestVersion.imageAudit?.verdict ?? null,
|
|
2627
|
+
publishedAt: pluginDetail.latestVersion.publishedAt
|
|
2628
|
+
};
|
|
2629
|
+
return (await client.getVersions(pluginId)).find((v) => v.version === version) ?? null;
|
|
2630
|
+
}
|
|
2631
|
+
/** A null verdict means no audit ran, which is allowed; "fail" and "warn" are not. */
|
|
2632
|
+
function checkAuditVerdict(versionMetadata) {
|
|
2633
|
+
if (versionMetadata.auditVerdict !== "fail" && versionMetadata.auditVerdict !== "warn") return null;
|
|
2634
|
+
return {
|
|
2635
|
+
success: false,
|
|
2636
|
+
error: {
|
|
2637
|
+
code: "AUDIT_FAILED",
|
|
2638
|
+
message: versionMetadata.auditVerdict === "fail" ? "Plugin failed security audit and cannot be installed or updated" : "Plugin audit was inconclusive and cannot be installed or updated until reviewed"
|
|
2639
|
+
}
|
|
2640
|
+
};
|
|
2641
|
+
}
|
|
2642
|
+
function validateBundleIdentity(bundle, pluginId, version) {
|
|
2643
|
+
if (bundle.manifest.id !== pluginId) return {
|
|
2644
|
+
success: false,
|
|
2645
|
+
error: {
|
|
2646
|
+
code: "MANIFEST_MISMATCH",
|
|
2647
|
+
message: `Bundle manifest ID (${bundle.manifest.id}) does not match requested plugin (${pluginId})`
|
|
2648
|
+
}
|
|
2649
|
+
};
|
|
2650
|
+
if (bundle.manifest.version !== version) return {
|
|
2651
|
+
success: false,
|
|
2652
|
+
error: {
|
|
2653
|
+
code: "MANIFEST_VERSION_MISMATCH",
|
|
2654
|
+
message: `Bundle manifest version (${bundle.manifest.version}) does not match requested version (${version})`
|
|
2655
|
+
}
|
|
2656
|
+
};
|
|
2657
|
+
return null;
|
|
2658
|
+
}
|
|
2659
|
+
function bundlePrefix(source, pluginId, version) {
|
|
2660
|
+
return `${source}/${pluginId}/${version}`;
|
|
2661
|
+
}
|
|
2662
|
+
async function storeBundleInR2(storage, pluginId, version, bundle, source = "marketplace") {
|
|
2663
|
+
validatePluginIdentifier(pluginId, "plugin ID");
|
|
2664
|
+
validateVersion(version);
|
|
2665
|
+
const prefix = bundlePrefix(source, pluginId, version);
|
|
2666
|
+
await storage.upload({
|
|
2667
|
+
key: `${prefix}/manifest.json`,
|
|
2668
|
+
body: new TextEncoder().encode(JSON.stringify(bundle.manifest)),
|
|
2669
|
+
contentType: "application/json"
|
|
2670
|
+
});
|
|
2671
|
+
await storage.upload({
|
|
2672
|
+
key: `${prefix}/backend.js`,
|
|
2673
|
+
body: new TextEncoder().encode(bundle.backendCode),
|
|
2674
|
+
contentType: "application/javascript"
|
|
2675
|
+
});
|
|
2676
|
+
if (bundle.adminCode) await storage.upload({
|
|
2677
|
+
key: `${prefix}/admin.js`,
|
|
2678
|
+
body: new TextEncoder().encode(bundle.adminCode),
|
|
2679
|
+
contentType: "application/javascript"
|
|
2680
|
+
});
|
|
2681
|
+
}
|
|
2682
|
+
/** Read a ReadableStream to string */
|
|
2683
|
+
async function streamToText(stream) {
|
|
2684
|
+
return new Response(stream).text();
|
|
2685
|
+
}
|
|
2686
|
+
/**
|
|
2687
|
+
* Load a plugin bundle from site-local R2 storage.
|
|
2688
|
+
*
|
|
2689
|
+
* `source` selects the R2 key prefix: marketplace plugins are stored
|
|
2690
|
+
* under `marketplace/<id>/<version>/`, registry plugins under
|
|
2691
|
+
* `registry/<id>/<version>/`. Defaults to `"marketplace"` for
|
|
2692
|
+
* backwards compatibility with pre-registry call sites.
|
|
2693
|
+
*/
|
|
2694
|
+
async function loadBundleFromR2(storage, pluginId, version, source = "marketplace") {
|
|
2695
|
+
validatePluginIdentifier(pluginId, "plugin ID");
|
|
2696
|
+
validateVersion(version);
|
|
2697
|
+
const prefix = bundlePrefix(source, pluginId, version);
|
|
2698
|
+
try {
|
|
2699
|
+
const manifestResult = await storage.download(`${prefix}/manifest.json`);
|
|
2700
|
+
const backendResult = await storage.download(`${prefix}/backend.js`);
|
|
2701
|
+
const manifestText = await streamToText(manifestResult.body);
|
|
2702
|
+
const backendCode = await streamToText(backendResult.body);
|
|
2703
|
+
const parsed = JSON.parse(manifestText);
|
|
2704
|
+
const result = pluginManifestSchema$1.safeParse(parsed);
|
|
2705
|
+
if (!result.success) return null;
|
|
2706
|
+
const manifest = reconcileManifestAccess$1(result.data);
|
|
2707
|
+
let adminCode;
|
|
2708
|
+
try {
|
|
2709
|
+
adminCode = await streamToText((await storage.download(`${prefix}/admin.js`)).body);
|
|
2710
|
+
} catch {}
|
|
2711
|
+
return {
|
|
2712
|
+
manifest,
|
|
2713
|
+
backendCode,
|
|
2714
|
+
adminCode
|
|
2715
|
+
};
|
|
2716
|
+
} catch {
|
|
2717
|
+
return null;
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
/** Delete a plugin bundle from site-local R2 storage */
|
|
2721
|
+
async function deleteBundleFromR2(storage, pluginId, version, source = "marketplace") {
|
|
2722
|
+
validatePluginIdentifier(pluginId, "plugin ID");
|
|
2723
|
+
validateVersion(version);
|
|
2724
|
+
const prefix = bundlePrefix(source, pluginId, version);
|
|
2725
|
+
for (const file of [
|
|
2726
|
+
"manifest.json",
|
|
2727
|
+
"backend.js",
|
|
2728
|
+
"admin.js"
|
|
2729
|
+
]) try {
|
|
2730
|
+
await storage.delete(`${prefix}/${file}`);
|
|
2731
|
+
} catch {}
|
|
2732
|
+
}
|
|
2733
|
+
async function rollbackPluginUpdate(db, storage, previousState, failedVersion, source) {
|
|
2734
|
+
if (!storage) return {
|
|
2735
|
+
success: false,
|
|
2736
|
+
error: {
|
|
2737
|
+
code: "STORAGE_NOT_CONFIGURED",
|
|
2738
|
+
message: "Storage is required"
|
|
2739
|
+
}
|
|
2740
|
+
};
|
|
2741
|
+
if (previousState.source !== source) return {
|
|
2742
|
+
success: false,
|
|
2743
|
+
error: {
|
|
2744
|
+
code: "UPDATE_ROLLBACK_CONFLICT",
|
|
2745
|
+
message: "Plugin source changed during update"
|
|
2746
|
+
}
|
|
2747
|
+
};
|
|
2748
|
+
try {
|
|
2749
|
+
if (!await new PluginStateRepository(db).restoreIfVersion(failedVersion, previousState)) return {
|
|
2750
|
+
success: false,
|
|
2751
|
+
error: {
|
|
2752
|
+
code: "UPDATE_ROLLBACK_CONFLICT",
|
|
2753
|
+
message: "Plugin state changed before the failed update could be rolled back"
|
|
2754
|
+
}
|
|
2755
|
+
};
|
|
2756
|
+
await deleteBundleFromR2(storage, previousState.pluginId, failedVersion, source);
|
|
2757
|
+
return {
|
|
2758
|
+
success: true,
|
|
2759
|
+
data: {
|
|
2760
|
+
pluginId: previousState.pluginId,
|
|
2761
|
+
version: previousState.version
|
|
2762
|
+
}
|
|
2763
|
+
};
|
|
2764
|
+
} catch (error) {
|
|
2765
|
+
console.error("Failed to roll back plugin update:", error);
|
|
2766
|
+
return {
|
|
2767
|
+
success: false,
|
|
2768
|
+
error: {
|
|
2769
|
+
code: "UPDATE_ROLLBACK_FAILED",
|
|
2770
|
+
message: "Failed to roll back plugin update"
|
|
2771
|
+
}
|
|
2772
|
+
};
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
async function handleMarketplaceInstall(db, storage, sandboxRunner, marketplaceUrl, pluginId, opts) {
|
|
2776
|
+
const client = getClient(marketplaceUrl, opts?.siteOrigin);
|
|
2777
|
+
if (!client) return {
|
|
2778
|
+
success: false,
|
|
2779
|
+
error: {
|
|
2780
|
+
code: "MARKETPLACE_NOT_CONFIGURED",
|
|
2781
|
+
message: "Marketplace is not configured"
|
|
2782
|
+
}
|
|
2783
|
+
};
|
|
2784
|
+
if (!storage) return {
|
|
2785
|
+
success: false,
|
|
2786
|
+
error: {
|
|
2787
|
+
code: "STORAGE_NOT_CONFIGURED",
|
|
2788
|
+
message: "Storage is required for marketplace plugin installation"
|
|
2789
|
+
}
|
|
2790
|
+
};
|
|
2791
|
+
if (!opts?.sandboxBypassed && (!sandboxRunner || !sandboxRunner.isAvailable())) return {
|
|
2792
|
+
success: false,
|
|
2793
|
+
error: {
|
|
2794
|
+
code: "SANDBOX_NOT_AVAILABLE",
|
|
2795
|
+
message: withUnavailableReason("Sandbox runner is required for marketplace plugins", sandboxRunner)
|
|
2796
|
+
}
|
|
2797
|
+
};
|
|
2798
|
+
try {
|
|
2799
|
+
const stateRepo = new PluginStateRepository(db);
|
|
2800
|
+
const existing = await stateRepo.get(pluginId);
|
|
2801
|
+
if (existing && existing.source === "marketplace") return {
|
|
2802
|
+
success: false,
|
|
2803
|
+
error: {
|
|
2804
|
+
code: "ALREADY_INSTALLED",
|
|
2805
|
+
message: `Plugin ${pluginId} is already installed`
|
|
2806
|
+
}
|
|
2807
|
+
};
|
|
2808
|
+
if (opts?.configuredPluginIds?.has(pluginId)) return {
|
|
2809
|
+
success: false,
|
|
2810
|
+
error: {
|
|
2811
|
+
code: "PLUGIN_ID_CONFLICT",
|
|
2812
|
+
message: `Cannot install marketplace plugin "${pluginId}" — a configured plugin with the same ID already exists`
|
|
2813
|
+
}
|
|
2814
|
+
};
|
|
2815
|
+
const pluginDetail = await client.getPlugin(pluginId);
|
|
2816
|
+
const version = opts?.version ?? pluginDetail.latestVersion?.version;
|
|
2817
|
+
if (!version) return {
|
|
2818
|
+
success: false,
|
|
2819
|
+
error: {
|
|
2820
|
+
code: "NO_VERSION",
|
|
2821
|
+
message: `No published versions found for plugin ${pluginId}`
|
|
2822
|
+
}
|
|
2823
|
+
};
|
|
2824
|
+
const versionMetadata = await resolveVersionMetadata(client, pluginId, pluginDetail, version);
|
|
2825
|
+
if (!versionMetadata) return {
|
|
2826
|
+
success: false,
|
|
2827
|
+
error: {
|
|
2828
|
+
code: "NO_VERSION",
|
|
2829
|
+
message: `Version ${version} was not found for plugin ${pluginId}`
|
|
2830
|
+
}
|
|
2831
|
+
};
|
|
2832
|
+
const auditError = checkAuditVerdict(versionMetadata);
|
|
2833
|
+
if (auditError) return auditError;
|
|
2834
|
+
const bundle = await client.downloadBundle(pluginId, version);
|
|
2835
|
+
if (versionMetadata.checksum && bundle.checksum !== versionMetadata.checksum) return {
|
|
2836
|
+
success: false,
|
|
2837
|
+
error: {
|
|
2838
|
+
code: "CHECKSUM_MISMATCH",
|
|
2839
|
+
message: "Bundle checksum does not match marketplace record. Download may be corrupted."
|
|
2840
|
+
}
|
|
2841
|
+
};
|
|
2842
|
+
const bundleIdentityError = validateBundleIdentity(bundle, pluginId, version);
|
|
2843
|
+
if (bundleIdentityError) return bundleIdentityError;
|
|
2844
|
+
const routeVisibilityChanges = diffRouteVisibility(void 0, bundle.manifest);
|
|
2845
|
+
const hasPublicRoutes = routeVisibilityChanges.newlyPublic.length > 0;
|
|
2846
|
+
const publicRoutes = routeVisibilityChanges.newlyPublic.toSorted();
|
|
2847
|
+
const acknowledgedPublicRoutes = (opts?.acknowledgedPublicRoutes ?? []).toSorted();
|
|
2848
|
+
const mcpTools = bundle.manifest.mcp?.tools.map(({ inputSchema: _, outputSchema: __, ...tool }) => tool);
|
|
2849
|
+
const consentDetails = {
|
|
2850
|
+
routeVisibilityChanges: hasPublicRoutes ? { newlyPublic: publicRoutes } : void 0,
|
|
2851
|
+
mcpTools
|
|
2852
|
+
};
|
|
2853
|
+
if (hasPublicRoutes && JSON.stringify(acknowledgedPublicRoutes) !== JSON.stringify(publicRoutes)) return {
|
|
2854
|
+
success: false,
|
|
2855
|
+
error: {
|
|
2856
|
+
code: "ROUTE_VISIBILITY_ESCALATION",
|
|
2857
|
+
message: "Plugin install exposes public (unauthenticated) routes",
|
|
2858
|
+
details: consentDetails
|
|
2859
|
+
}
|
|
2860
|
+
};
|
|
2861
|
+
if ((mcpTools?.length ?? 0) > 0 && !opts?.confirmMcpTools) return {
|
|
2862
|
+
success: false,
|
|
2863
|
+
error: {
|
|
2864
|
+
code: "MCP_TOOL_CONSENT_REQUIRED",
|
|
2865
|
+
message: "Plugin MCP tools require explicit consent",
|
|
2866
|
+
details: consentDetails
|
|
2867
|
+
}
|
|
2868
|
+
};
|
|
2869
|
+
await storeBundleInR2(storage, pluginId, version, bundle);
|
|
2870
|
+
await stateRepo.upsert(pluginId, version, "active", {
|
|
2871
|
+
source: "marketplace",
|
|
2872
|
+
marketplaceVersion: version,
|
|
2873
|
+
displayName: pluginDetail.name,
|
|
2874
|
+
description: pluginDetail.description ?? void 0
|
|
2875
|
+
});
|
|
2876
|
+
await syncDeclaredStorageIndexes(db, [bundle.manifest]);
|
|
2877
|
+
client.reportInstall(pluginId, version).catch(() => {});
|
|
2878
|
+
return {
|
|
2879
|
+
success: true,
|
|
2880
|
+
data: {
|
|
2881
|
+
pluginId,
|
|
2882
|
+
version,
|
|
2883
|
+
capabilities: bundle.manifest.capabilities
|
|
2884
|
+
}
|
|
2885
|
+
};
|
|
2886
|
+
} catch (err) {
|
|
2887
|
+
if (err instanceof MarketplaceUnavailableError) return {
|
|
2888
|
+
success: false,
|
|
2889
|
+
error: {
|
|
2890
|
+
code: "MARKETPLACE_UNAVAILABLE",
|
|
2891
|
+
message: "Plugin marketplace is currently unavailable"
|
|
2892
|
+
}
|
|
2893
|
+
};
|
|
2894
|
+
if (err instanceof MarketplaceError) return {
|
|
2895
|
+
success: false,
|
|
2896
|
+
error: {
|
|
2897
|
+
code: err.code ?? "MARKETPLACE_ERROR",
|
|
2898
|
+
message: err.message
|
|
2899
|
+
}
|
|
2900
|
+
};
|
|
2901
|
+
if (err instanceof EmDashStorageError) return {
|
|
2902
|
+
success: false,
|
|
2903
|
+
error: {
|
|
2904
|
+
code: err.code ?? "STORAGE_ERROR",
|
|
2905
|
+
message: "Storage error while installing plugin"
|
|
2906
|
+
}
|
|
2907
|
+
};
|
|
2908
|
+
if (err && typeof err === "object" && "code" in err) {
|
|
2909
|
+
const code = err.code;
|
|
2910
|
+
if (typeof code === "string" && code.trim()) return {
|
|
2911
|
+
success: false,
|
|
2912
|
+
error: {
|
|
2913
|
+
code,
|
|
2914
|
+
message: "Failed to install plugin from marketplace"
|
|
2915
|
+
}
|
|
2916
|
+
};
|
|
2917
|
+
}
|
|
2918
|
+
console.error("Failed to install marketplace plugin:", err);
|
|
2919
|
+
return {
|
|
2920
|
+
success: false,
|
|
2921
|
+
error: {
|
|
2922
|
+
code: "INSTALL_FAILED",
|
|
2923
|
+
message: "Failed to install plugin from marketplace"
|
|
2924
|
+
}
|
|
2925
|
+
};
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
async function handleMarketplaceUpdate(db, storage, sandboxRunner, marketplaceUrl, pluginId, opts) {
|
|
2929
|
+
const client = getClient(marketplaceUrl);
|
|
2930
|
+
if (!client) return {
|
|
2931
|
+
success: false,
|
|
2932
|
+
error: {
|
|
2933
|
+
code: "MARKETPLACE_NOT_CONFIGURED",
|
|
2934
|
+
message: "Marketplace is not configured"
|
|
2935
|
+
}
|
|
2936
|
+
};
|
|
2937
|
+
if (!storage) return {
|
|
2938
|
+
success: false,
|
|
2939
|
+
error: {
|
|
2940
|
+
code: "STORAGE_NOT_CONFIGURED",
|
|
2941
|
+
message: "Storage is required"
|
|
2942
|
+
}
|
|
2943
|
+
};
|
|
2944
|
+
if (!opts?.sandboxBypassed && (!sandboxRunner || !sandboxRunner.isAvailable())) return {
|
|
2945
|
+
success: false,
|
|
2946
|
+
error: {
|
|
2947
|
+
code: "SANDBOX_NOT_AVAILABLE",
|
|
2948
|
+
message: withUnavailableReason("Sandbox runner is required", sandboxRunner)
|
|
2949
|
+
}
|
|
2950
|
+
};
|
|
2951
|
+
try {
|
|
2952
|
+
const stateRepo = new PluginStateRepository(db);
|
|
2953
|
+
const existing = await stateRepo.get(pluginId);
|
|
2954
|
+
if (!existing || existing.source !== "marketplace") return {
|
|
2955
|
+
success: false,
|
|
2956
|
+
error: {
|
|
2957
|
+
code: "NOT_FOUND",
|
|
2958
|
+
message: `No marketplace plugin found: ${pluginId}`
|
|
2959
|
+
}
|
|
2960
|
+
};
|
|
2961
|
+
const oldVersion = existing.marketplaceVersion ?? existing.version;
|
|
2962
|
+
const pluginDetail = await client.getPlugin(pluginId);
|
|
2963
|
+
const newVersion = opts?.version ?? pluginDetail.latestVersion?.version;
|
|
2964
|
+
if (!newVersion) return {
|
|
2965
|
+
success: false,
|
|
2966
|
+
error: {
|
|
2967
|
+
code: "NO_VERSION",
|
|
2968
|
+
message: "No newer version available"
|
|
2969
|
+
}
|
|
2970
|
+
};
|
|
2971
|
+
if (newVersion === oldVersion) return {
|
|
2972
|
+
success: false,
|
|
2973
|
+
error: {
|
|
2974
|
+
code: "ALREADY_UP_TO_DATE",
|
|
2975
|
+
message: "Plugin is already up to date"
|
|
2976
|
+
}
|
|
2977
|
+
};
|
|
2978
|
+
const versionMetadata = await resolveVersionMetadata(client, pluginId, pluginDetail, newVersion);
|
|
2979
|
+
if (!versionMetadata) return {
|
|
2980
|
+
success: false,
|
|
2981
|
+
error: {
|
|
2982
|
+
code: "NO_VERSION",
|
|
2983
|
+
message: `Version ${newVersion} was not found for plugin ${pluginId}`
|
|
2984
|
+
}
|
|
2985
|
+
};
|
|
2986
|
+
const auditError = checkAuditVerdict(versionMetadata);
|
|
2987
|
+
if (auditError) return auditError;
|
|
2988
|
+
const bundle = await client.downloadBundle(pluginId, newVersion);
|
|
2989
|
+
if (versionMetadata.checksum && bundle.checksum !== versionMetadata.checksum) return {
|
|
2990
|
+
success: false,
|
|
2991
|
+
error: {
|
|
2992
|
+
code: "CHECKSUM_MISMATCH",
|
|
2993
|
+
message: "Bundle checksum does not match marketplace record. Download may be corrupted."
|
|
2994
|
+
}
|
|
2995
|
+
};
|
|
2996
|
+
const bundleIdentityError = validateBundleIdentity(bundle, pluginId, newVersion);
|
|
2997
|
+
if (bundleIdentityError) return bundleIdentityError;
|
|
2998
|
+
const oldBundle = await loadBundleFromR2(storage, pluginId, oldVersion);
|
|
2999
|
+
const capabilityChanges = diffCapabilities(oldBundle?.manifest.capabilities ?? [], bundle.manifest.capabilities);
|
|
3000
|
+
const hasEscalation = capabilityChanges.added.length > 0;
|
|
3001
|
+
const routeVisibilityChanges = diffRouteVisibility(oldBundle?.manifest, bundle.manifest);
|
|
3002
|
+
const hasNewPublicRoutes = routeVisibilityChanges.newlyPublic.length > 0;
|
|
3003
|
+
const newlyPublicRoutes = routeVisibilityChanges.newlyPublic.toSorted();
|
|
3004
|
+
const acknowledgedPublicRoutes = (opts?.acknowledgedPublicRoutes ?? []).toSorted();
|
|
3005
|
+
const oldMcpTools = [...oldBundle?.manifest.mcp?.tools ?? []].toSorted((a, b) => a.name.localeCompare(b.name));
|
|
3006
|
+
const newMcpTools = [...bundle.manifest.mcp?.tools ?? []].toSorted((a, b) => a.name.localeCompare(b.name));
|
|
3007
|
+
const hasMcpChanges = JSON.stringify(oldMcpTools) !== JSON.stringify(newMcpTools);
|
|
3008
|
+
const mcpTools = newMcpTools.map(({ inputSchema: _, outputSchema: __, ...tool }) => tool);
|
|
3009
|
+
const consentDetails = {
|
|
3010
|
+
capabilityChanges,
|
|
3011
|
+
routeVisibilityChanges: hasNewPublicRoutes ? { newlyPublic: newlyPublicRoutes } : void 0,
|
|
3012
|
+
mcpTools: hasMcpChanges ? mcpTools : void 0
|
|
3013
|
+
};
|
|
3014
|
+
if (hasEscalation && !opts?.confirmCapabilityChanges) return {
|
|
3015
|
+
success: false,
|
|
3016
|
+
error: {
|
|
3017
|
+
code: "CAPABILITY_ESCALATION",
|
|
3018
|
+
message: "Plugin update requires new capabilities",
|
|
3019
|
+
details: consentDetails
|
|
3020
|
+
}
|
|
3021
|
+
};
|
|
3022
|
+
if (hasNewPublicRoutes && JSON.stringify(acknowledgedPublicRoutes) !== JSON.stringify(newlyPublicRoutes)) return {
|
|
3023
|
+
success: false,
|
|
3024
|
+
error: {
|
|
3025
|
+
code: "ROUTE_VISIBILITY_ESCALATION",
|
|
3026
|
+
message: "Plugin update exposes new public (unauthenticated) routes",
|
|
3027
|
+
details: consentDetails
|
|
3028
|
+
}
|
|
3029
|
+
};
|
|
3030
|
+
if (hasMcpChanges && !opts?.confirmMcpTools) return {
|
|
3031
|
+
success: false,
|
|
3032
|
+
error: {
|
|
3033
|
+
code: "MCP_TOOL_CONSENT_REQUIRED",
|
|
3034
|
+
message: "Plugin update changes its MCP tools",
|
|
3035
|
+
details: consentDetails
|
|
3036
|
+
}
|
|
3037
|
+
};
|
|
3038
|
+
await storeBundleInR2(storage, pluginId, newVersion, bundle);
|
|
3039
|
+
await stateRepo.upsert(pluginId, newVersion, "active", {
|
|
3040
|
+
source: "marketplace",
|
|
3041
|
+
marketplaceVersion: newVersion,
|
|
3042
|
+
displayName: pluginDetail.name,
|
|
3043
|
+
description: pluginDetail.description ?? void 0,
|
|
3044
|
+
mcpToolsEnabled: false,
|
|
3045
|
+
mcpToolsConsent: null
|
|
3046
|
+
});
|
|
3047
|
+
await syncDeclaredStorageIndexes(db, [bundle.manifest]);
|
|
3048
|
+
return {
|
|
3049
|
+
success: true,
|
|
3050
|
+
data: {
|
|
3051
|
+
pluginId,
|
|
3052
|
+
oldVersion,
|
|
3053
|
+
newVersion,
|
|
3054
|
+
capabilityChanges,
|
|
3055
|
+
routeVisibilityChanges: hasNewPublicRoutes ? routeVisibilityChanges : void 0
|
|
3056
|
+
}
|
|
3057
|
+
};
|
|
3058
|
+
} catch (err) {
|
|
3059
|
+
if (err instanceof MarketplaceUnavailableError) return {
|
|
3060
|
+
success: false,
|
|
3061
|
+
error: {
|
|
3062
|
+
code: "MARKETPLACE_UNAVAILABLE",
|
|
3063
|
+
message: "Marketplace is unavailable"
|
|
3064
|
+
}
|
|
3065
|
+
};
|
|
3066
|
+
if (err instanceof MarketplaceError) return {
|
|
3067
|
+
success: false,
|
|
3068
|
+
error: {
|
|
3069
|
+
code: err.code ?? "MARKETPLACE_ERROR",
|
|
3070
|
+
message: err.message
|
|
3071
|
+
}
|
|
3072
|
+
};
|
|
3073
|
+
console.error("Failed to update marketplace plugin:", err);
|
|
3074
|
+
return {
|
|
3075
|
+
success: false,
|
|
3076
|
+
error: {
|
|
3077
|
+
code: "UPDATE_FAILED",
|
|
3078
|
+
message: "Failed to update plugin"
|
|
3079
|
+
}
|
|
3080
|
+
};
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
async function handleMarketplaceUninstall(db, storage, pluginId, opts) {
|
|
3084
|
+
try {
|
|
3085
|
+
const stateRepo = new PluginStateRepository(db);
|
|
3086
|
+
const existing = await stateRepo.get(pluginId);
|
|
3087
|
+
if (!existing || existing.source !== "marketplace") return {
|
|
3088
|
+
success: false,
|
|
3089
|
+
error: {
|
|
3090
|
+
code: "NOT_FOUND",
|
|
3091
|
+
message: `No marketplace plugin found: ${pluginId}`
|
|
3092
|
+
}
|
|
3093
|
+
};
|
|
3094
|
+
const version = existing.marketplaceVersion ?? existing.version;
|
|
3095
|
+
await opts?.beforeDelete?.();
|
|
3096
|
+
let dataDeleted = false;
|
|
3097
|
+
if (opts?.deleteData) try {
|
|
3098
|
+
await db.deleteFrom("_plugin_storage").where("plugin_id", "=", pluginId).execute();
|
|
3099
|
+
dataDeleted = true;
|
|
3100
|
+
} catch {}
|
|
3101
|
+
try {
|
|
3102
|
+
await removeAllPluginIndexes(db, pluginId);
|
|
3103
|
+
} catch {}
|
|
3104
|
+
await stateRepo.delete(pluginId);
|
|
3105
|
+
if (storage) await deleteBundleFromR2(storage, pluginId, version);
|
|
3106
|
+
return {
|
|
3107
|
+
success: true,
|
|
3108
|
+
data: {
|
|
3109
|
+
pluginId,
|
|
3110
|
+
dataDeleted
|
|
3111
|
+
}
|
|
3112
|
+
};
|
|
3113
|
+
} catch (err) {
|
|
3114
|
+
console.error("Failed to uninstall marketplace plugin:", err);
|
|
3115
|
+
return {
|
|
3116
|
+
success: false,
|
|
3117
|
+
error: {
|
|
3118
|
+
code: "UNINSTALL_FAILED",
|
|
3119
|
+
message: "Failed to uninstall plugin"
|
|
3120
|
+
}
|
|
3121
|
+
};
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
async function handleMarketplaceUpdateCheck(db, marketplaceUrl) {
|
|
3125
|
+
const client = getClient(marketplaceUrl);
|
|
3126
|
+
if (!client) return {
|
|
3127
|
+
success: false,
|
|
3128
|
+
error: {
|
|
3129
|
+
code: "MARKETPLACE_NOT_CONFIGURED",
|
|
3130
|
+
message: "Marketplace is not configured"
|
|
3131
|
+
}
|
|
3132
|
+
};
|
|
3133
|
+
try {
|
|
3134
|
+
const marketplacePlugins = await new PluginStateRepository(db).getMarketplacePlugins();
|
|
3135
|
+
const items = [];
|
|
3136
|
+
for (const plugin of marketplacePlugins) try {
|
|
3137
|
+
const detail = await client.getPlugin(plugin.pluginId);
|
|
3138
|
+
const latest = detail.latestVersion?.version;
|
|
3139
|
+
const installed = plugin.marketplaceVersion ?? plugin.version;
|
|
3140
|
+
if (!latest) continue;
|
|
3141
|
+
const hasUpdate = latest !== installed;
|
|
3142
|
+
let capabilityChanges;
|
|
3143
|
+
let hasCapabilityChanges = false;
|
|
3144
|
+
if (hasUpdate && detail.latestVersion) {
|
|
3145
|
+
capabilityChanges = diffCapabilities(detail.capabilities ?? [], detail.latestVersion.capabilities ?? []);
|
|
3146
|
+
hasCapabilityChanges = capabilityChanges.added.length > 0 || capabilityChanges.removed.length > 0;
|
|
3147
|
+
}
|
|
3148
|
+
items.push({
|
|
3149
|
+
pluginId: plugin.pluginId,
|
|
3150
|
+
installed,
|
|
3151
|
+
latest: latest ?? installed,
|
|
3152
|
+
hasUpdate,
|
|
3153
|
+
hasCapabilityChanges,
|
|
3154
|
+
capabilityChanges: hasCapabilityChanges ? capabilityChanges : void 0,
|
|
3155
|
+
hasRouteVisibilityChanges: false
|
|
3156
|
+
});
|
|
3157
|
+
} catch (err) {
|
|
3158
|
+
console.warn(`Failed to check updates for ${plugin.pluginId}:`, err);
|
|
3159
|
+
}
|
|
3160
|
+
return {
|
|
3161
|
+
success: true,
|
|
3162
|
+
data: { items }
|
|
3163
|
+
};
|
|
3164
|
+
} catch (err) {
|
|
3165
|
+
if (err instanceof MarketplaceUnavailableError) return {
|
|
3166
|
+
success: false,
|
|
3167
|
+
error: {
|
|
3168
|
+
code: "MARKETPLACE_UNAVAILABLE",
|
|
3169
|
+
message: "Marketplace is unavailable"
|
|
3170
|
+
}
|
|
3171
|
+
};
|
|
3172
|
+
console.error("Failed to check marketplace updates:", err);
|
|
3173
|
+
return {
|
|
3174
|
+
success: false,
|
|
3175
|
+
error: {
|
|
3176
|
+
code: "UPDATE_CHECK_FAILED",
|
|
3177
|
+
message: "Failed to check for updates"
|
|
3178
|
+
}
|
|
3179
|
+
};
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
async function handleMarketplaceSearch(marketplaceUrl, query, opts) {
|
|
3183
|
+
const client = getClient(marketplaceUrl);
|
|
3184
|
+
if (!client) return {
|
|
3185
|
+
success: false,
|
|
3186
|
+
error: {
|
|
3187
|
+
code: "MARKETPLACE_NOT_CONFIGURED",
|
|
3188
|
+
message: "Marketplace is not configured"
|
|
3189
|
+
}
|
|
3190
|
+
};
|
|
3191
|
+
try {
|
|
3192
|
+
return {
|
|
3193
|
+
success: true,
|
|
3194
|
+
data: await client.search(query, opts)
|
|
3195
|
+
};
|
|
3196
|
+
} catch (err) {
|
|
3197
|
+
if (err instanceof MarketplaceUnavailableError) return {
|
|
3198
|
+
success: false,
|
|
3199
|
+
error: {
|
|
3200
|
+
code: "MARKETPLACE_UNAVAILABLE",
|
|
3201
|
+
message: "Marketplace is unavailable"
|
|
3202
|
+
}
|
|
3203
|
+
};
|
|
3204
|
+
console.error("Failed to search marketplace:", err);
|
|
3205
|
+
return {
|
|
3206
|
+
success: false,
|
|
3207
|
+
error: {
|
|
3208
|
+
code: "SEARCH_FAILED",
|
|
3209
|
+
message: "Failed to search marketplace"
|
|
3210
|
+
}
|
|
3211
|
+
};
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
async function handleMarketplaceGetPlugin(marketplaceUrl, pluginId) {
|
|
3215
|
+
const client = getClient(marketplaceUrl);
|
|
3216
|
+
if (!client) return {
|
|
3217
|
+
success: false,
|
|
3218
|
+
error: {
|
|
3219
|
+
code: "MARKETPLACE_NOT_CONFIGURED",
|
|
3220
|
+
message: "Marketplace is not configured"
|
|
3221
|
+
}
|
|
3222
|
+
};
|
|
3223
|
+
try {
|
|
3224
|
+
return {
|
|
3225
|
+
success: true,
|
|
3226
|
+
data: await client.getPlugin(pluginId)
|
|
3227
|
+
};
|
|
3228
|
+
} catch (err) {
|
|
3229
|
+
if (err instanceof MarketplaceError && err.status === 404) return {
|
|
3230
|
+
success: false,
|
|
3231
|
+
error: {
|
|
3232
|
+
code: "NOT_FOUND",
|
|
3233
|
+
message: `Plugin not found: ${pluginId}`
|
|
3234
|
+
}
|
|
3235
|
+
};
|
|
3236
|
+
if (err instanceof MarketplaceUnavailableError) return {
|
|
3237
|
+
success: false,
|
|
3238
|
+
error: {
|
|
3239
|
+
code: "MARKETPLACE_UNAVAILABLE",
|
|
3240
|
+
message: "Marketplace is unavailable"
|
|
3241
|
+
}
|
|
3242
|
+
};
|
|
3243
|
+
console.error("Failed to get marketplace plugin:", err);
|
|
3244
|
+
return {
|
|
3245
|
+
success: false,
|
|
3246
|
+
error: {
|
|
3247
|
+
code: "GET_PLUGIN_FAILED",
|
|
3248
|
+
message: "Failed to get plugin details"
|
|
3249
|
+
}
|
|
3250
|
+
};
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
async function handleThemeSearch(marketplaceUrl, query, opts) {
|
|
3254
|
+
const client = getClient(marketplaceUrl);
|
|
3255
|
+
if (!client) return {
|
|
3256
|
+
success: false,
|
|
3257
|
+
error: {
|
|
3258
|
+
code: "MARKETPLACE_NOT_CONFIGURED",
|
|
3259
|
+
message: "Marketplace is not configured"
|
|
3260
|
+
}
|
|
3261
|
+
};
|
|
3262
|
+
try {
|
|
3263
|
+
return {
|
|
3264
|
+
success: true,
|
|
3265
|
+
data: await client.searchThemes(query, opts)
|
|
3266
|
+
};
|
|
3267
|
+
} catch (err) {
|
|
3268
|
+
if (err instanceof MarketplaceUnavailableError) return {
|
|
3269
|
+
success: false,
|
|
3270
|
+
error: {
|
|
3271
|
+
code: "MARKETPLACE_UNAVAILABLE",
|
|
3272
|
+
message: "Marketplace is unavailable"
|
|
3273
|
+
}
|
|
3274
|
+
};
|
|
3275
|
+
console.error("Failed to search themes:", err);
|
|
3276
|
+
return {
|
|
3277
|
+
success: false,
|
|
3278
|
+
error: {
|
|
3279
|
+
code: "THEME_SEARCH_FAILED",
|
|
3280
|
+
message: "Failed to search themes"
|
|
3281
|
+
}
|
|
3282
|
+
};
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
async function handleThemeGetDetail(marketplaceUrl, themeId) {
|
|
3286
|
+
const client = getClient(marketplaceUrl);
|
|
3287
|
+
if (!client) return {
|
|
3288
|
+
success: false,
|
|
3289
|
+
error: {
|
|
3290
|
+
code: "MARKETPLACE_NOT_CONFIGURED",
|
|
3291
|
+
message: "Marketplace is not configured"
|
|
3292
|
+
}
|
|
3293
|
+
};
|
|
3294
|
+
try {
|
|
3295
|
+
return {
|
|
3296
|
+
success: true,
|
|
3297
|
+
data: await client.getTheme(themeId)
|
|
3298
|
+
};
|
|
3299
|
+
} catch (err) {
|
|
3300
|
+
if (err instanceof MarketplaceError && err.status === 404) return {
|
|
3301
|
+
success: false,
|
|
3302
|
+
error: {
|
|
3303
|
+
code: "NOT_FOUND",
|
|
3304
|
+
message: `Theme not found: ${themeId}`
|
|
3305
|
+
}
|
|
3306
|
+
};
|
|
3307
|
+
if (err instanceof MarketplaceUnavailableError) return {
|
|
3308
|
+
success: false,
|
|
3309
|
+
error: {
|
|
3310
|
+
code: "MARKETPLACE_UNAVAILABLE",
|
|
3311
|
+
message: "Marketplace is unavailable"
|
|
3312
|
+
}
|
|
3313
|
+
};
|
|
3314
|
+
console.error("Failed to get marketplace theme:", err);
|
|
3315
|
+
return {
|
|
3316
|
+
success: false,
|
|
3317
|
+
error: {
|
|
3318
|
+
code: "GET_THEME_FAILED",
|
|
3319
|
+
message: "Failed to get theme details"
|
|
3320
|
+
}
|
|
3321
|
+
};
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3325
|
+
//#endregion
|
|
3326
|
+
//#region ../registry-verification/dist/bundle.js
|
|
3327
|
+
/** Maximum accepted gzip payload size. */
|
|
3328
|
+
const MAX_BUNDLE_COMPRESSED_BYTES = 384 * 1024;
|
|
3329
|
+
/** Maximum aggregate size of regular-file contents. */
|
|
3330
|
+
const MAX_BUNDLE_SIZE = 256 * 1024;
|
|
3331
|
+
/** Maximum size of one regular file. */
|
|
3332
|
+
const MAX_BUNDLE_FILE_BYTES = 128 * 1024;
|
|
3333
|
+
/** Maximum number of regular files. */
|
|
3334
|
+
const MAX_BUNDLE_FILE_COUNT = 20;
|
|
3335
|
+
/** Maximum total tar entries, including harmless directory entries. */
|
|
3336
|
+
const MAX_BUNDLE_TAR_ENTRY_COUNT = 32;
|
|
3337
|
+
/**
|
|
3338
|
+
* Maximum tar stream size after decompression. This includes USTAR headers,
|
|
3339
|
+
* file padding, and the end marker in addition to the regular-file contents.
|
|
3340
|
+
*/
|
|
3341
|
+
const MAX_BUNDLE_DECOMPRESSED_BYTES = MAX_BUNDLE_SIZE + MAX_BUNDLE_TAR_ENTRY_COUNT * 512 + MAX_BUNDLE_FILE_COUNT * 511 + 2 * 512;
|
|
3342
|
+
const TAR_BLOCK_BYTES = 512;
|
|
3343
|
+
const TAR_END_BYTES = TAR_BLOCK_BYTES * 2;
|
|
3344
|
+
const OCTAL_PATTERN = /^[0-7]+$/;
|
|
3345
|
+
const WINDOWS_DRIVE_PATTERN = /^[a-zA-Z]:/;
|
|
3346
|
+
const decoder = new TextDecoder("utf-8", {
|
|
3347
|
+
fatal: true,
|
|
3348
|
+
ignoreBOM: false
|
|
3349
|
+
});
|
|
3350
|
+
async function validatePluginBundle(compressed, options = {}) {
|
|
3351
|
+
if (compressed.byteLength > MAX_BUNDLE_COMPRESSED_BYTES) return verificationError("BUNDLE_COMPRESSED_SIZE_EXCEEDED", "The compressed plugin bundle exceeds the size limit.");
|
|
3352
|
+
const decompressed = await decompressBundle(compressed);
|
|
3353
|
+
if (!decompressed.success) return decompressed;
|
|
3354
|
+
const files = parseTar(decompressed.value);
|
|
3355
|
+
if (!files.success) return files;
|
|
3356
|
+
const manifestFile = files.value.get("manifest.json");
|
|
3357
|
+
if (!manifestFile) return verificationError("BUNDLE_MISSING_MANIFEST", "The plugin bundle is missing manifest.json.");
|
|
3358
|
+
const backend = files.value.get("backend.js");
|
|
3359
|
+
if (!backend) return verificationError("BUNDLE_MISSING_BACKEND", "The plugin bundle is missing backend.js.");
|
|
3360
|
+
let parsed;
|
|
3361
|
+
try {
|
|
3362
|
+
parsed = JSON.parse(decoder.decode(manifestFile.data));
|
|
3363
|
+
} catch {
|
|
3364
|
+
return verificationError("BUNDLE_INVALID_MANIFEST", "The plugin bundle manifest is not valid JSON.");
|
|
3365
|
+
}
|
|
3366
|
+
const validated = pluginManifestSchema.safeParse(parsed);
|
|
3367
|
+
if (!validated.success) return verificationError("BUNDLE_INVALID_MANIFEST", "The plugin bundle manifest failed schema validation.");
|
|
3368
|
+
const manifest = reconcileManifestAccess(validated.data);
|
|
3369
|
+
if (options.expectedSlug !== void 0 && manifest.id !== options.expectedSlug) return verificationError("BUNDLE_ID_MISMATCH", "The plugin bundle manifest id does not match the expected plugin.");
|
|
3370
|
+
if (options.expectedVersion !== void 0 && manifest.version !== options.expectedVersion) return verificationError("BUNDLE_VERSION_MISMATCH", "The plugin bundle manifest version does not match the expected version.");
|
|
3371
|
+
const result = {
|
|
3372
|
+
manifest,
|
|
3373
|
+
declaredAccess: manifest.declaredAccess ?? {},
|
|
3374
|
+
backend: backend.data
|
|
3375
|
+
};
|
|
3376
|
+
const admin = files.value.get("admin.js");
|
|
3377
|
+
if (admin) result.admin = admin.data;
|
|
3378
|
+
return {
|
|
3379
|
+
success: true,
|
|
3380
|
+
value: result
|
|
3381
|
+
};
|
|
3382
|
+
}
|
|
3383
|
+
async function decompressBundle(bytes) {
|
|
3384
|
+
const source = new ReadableStream({ start(controller) {
|
|
3385
|
+
controller.enqueue(bytes);
|
|
3386
|
+
controller.close();
|
|
3387
|
+
} });
|
|
3388
|
+
let reader;
|
|
3389
|
+
try {
|
|
3390
|
+
reader = source.pipeThrough(createGzipDecoder()).getReader();
|
|
3391
|
+
} catch {
|
|
3392
|
+
return verificationError("BUNDLE_INVALID_ARCHIVE", "The plugin bundle is not valid gzip data.");
|
|
3393
|
+
}
|
|
3394
|
+
const chunks = [];
|
|
3395
|
+
let total = 0;
|
|
3396
|
+
try {
|
|
3397
|
+
while (true) {
|
|
3398
|
+
const { done, value } = await reader.read();
|
|
3399
|
+
if (done) break;
|
|
3400
|
+
if (!value) continue;
|
|
3401
|
+
total += value.byteLength;
|
|
3402
|
+
if (total > MAX_BUNDLE_DECOMPRESSED_BYTES) {
|
|
3403
|
+
await reader.cancel().catch(() => void 0);
|
|
3404
|
+
return verificationError("BUNDLE_DECOMPRESSED_SIZE_EXCEEDED", "The decompressed plugin bundle exceeds the size limit.");
|
|
3405
|
+
}
|
|
3406
|
+
chunks.push(value);
|
|
3407
|
+
}
|
|
3408
|
+
} catch {
|
|
3409
|
+
return verificationError("BUNDLE_INVALID_ARCHIVE", "The plugin bundle is not valid gzip data.");
|
|
3410
|
+
}
|
|
3411
|
+
const output = new Uint8Array(total);
|
|
3412
|
+
let offset = 0;
|
|
3413
|
+
for (const chunk of chunks) {
|
|
3414
|
+
output.set(chunk, offset);
|
|
3415
|
+
offset += chunk.byteLength;
|
|
3416
|
+
}
|
|
3417
|
+
return {
|
|
3418
|
+
success: true,
|
|
3419
|
+
value: output
|
|
3420
|
+
};
|
|
3421
|
+
}
|
|
3422
|
+
function parseTar(bytes) {
|
|
3423
|
+
if (bytes.byteLength < TAR_END_BYTES || bytes.byteLength % TAR_BLOCK_BYTES !== 0) return invalidArchive();
|
|
3424
|
+
const files = /* @__PURE__ */ new Map();
|
|
3425
|
+
const rawPaths = /* @__PURE__ */ new Set();
|
|
3426
|
+
const normalizedPaths = /* @__PURE__ */ new Set();
|
|
3427
|
+
let offset = 0;
|
|
3428
|
+
let fileCount = 0;
|
|
3429
|
+
let entryCount = 0;
|
|
3430
|
+
let totalFileBytes = 0;
|
|
3431
|
+
let ended = false;
|
|
3432
|
+
while (offset + TAR_BLOCK_BYTES <= bytes.byteLength) {
|
|
3433
|
+
const header = bytes.subarray(offset, offset + TAR_BLOCK_BYTES);
|
|
3434
|
+
if (isZeroBlock(header)) {
|
|
3435
|
+
if (!isZeroBlock(bytes.subarray(offset + TAR_BLOCK_BYTES, offset + TAR_END_BYTES))) return invalidArchive();
|
|
3436
|
+
if (bytes.subarray(offset + TAR_END_BYTES).some((byte) => byte !== 0)) return invalidArchive();
|
|
3437
|
+
ended = true;
|
|
3438
|
+
break;
|
|
3439
|
+
}
|
|
3440
|
+
if (!validChecksum(header)) return invalidArchive();
|
|
3441
|
+
entryCount += 1;
|
|
3442
|
+
if (entryCount > MAX_BUNDLE_TAR_ENTRY_COUNT) return verificationError("BUNDLE_FILE_COUNT_EXCEEDED", "The plugin bundle contains too many archive entries.");
|
|
3443
|
+
const rawName = readTarPath(header);
|
|
3444
|
+
if (!rawName.success) return rawName;
|
|
3445
|
+
const typeFlag = header[156];
|
|
3446
|
+
const type = typeFlag === 0 ? "file" : String.fromCharCode(typeFlag ?? 0);
|
|
3447
|
+
const isDirectory = type === "5";
|
|
3448
|
+
const normalized = normalizePath(rawName.value, isDirectory);
|
|
3449
|
+
if (!normalized.success) return normalized;
|
|
3450
|
+
if (rawPaths.has(rawName.value) || normalizedPaths.has(normalized.value)) return verificationError("BUNDLE_PATH_COLLISION", "The plugin bundle contains duplicate or ambiguous paths.");
|
|
3451
|
+
rawPaths.add(rawName.value);
|
|
3452
|
+
normalizedPaths.add(normalized.value);
|
|
3453
|
+
const size = readOctal(header.subarray(124, 136));
|
|
3454
|
+
if (size === null || !Number.isSafeInteger(size)) return invalidArchive();
|
|
3455
|
+
const bodyStart = offset + TAR_BLOCK_BYTES;
|
|
3456
|
+
const nextOffset = bodyStart + Math.ceil(size / TAR_BLOCK_BYTES) * TAR_BLOCK_BYTES;
|
|
3457
|
+
if (nextOffset > bytes.byteLength) return invalidArchive();
|
|
3458
|
+
if (isDirectory) {
|
|
3459
|
+
if (size !== 0) return invalidArchive();
|
|
3460
|
+
} else if (type === "file" || type === "0") {
|
|
3461
|
+
fileCount += 1;
|
|
3462
|
+
if (fileCount > MAX_BUNDLE_FILE_COUNT) return verificationError("BUNDLE_FILE_COUNT_EXCEEDED", "The plugin bundle contains too many files.");
|
|
3463
|
+
if (size > MAX_BUNDLE_FILE_BYTES) return verificationError("BUNDLE_FILE_SIZE_EXCEEDED", "A file in the plugin bundle exceeds the per-file size limit.");
|
|
3464
|
+
totalFileBytes += size;
|
|
3465
|
+
if (totalFileBytes > MAX_BUNDLE_SIZE) return verificationError("BUNDLE_DECOMPRESSED_SIZE_EXCEEDED", "The plugin bundle file contents exceed the size limit.");
|
|
3466
|
+
files.set(normalized.value, {
|
|
3467
|
+
name: normalized.value,
|
|
3468
|
+
data: bytes.slice(bodyStart, bodyStart + size)
|
|
3469
|
+
});
|
|
3470
|
+
} else return verificationError("BUNDLE_UNSUPPORTED_ENTRY", "The plugin bundle contains an unsupported archive entry type.");
|
|
3471
|
+
offset = nextOffset;
|
|
3472
|
+
}
|
|
3473
|
+
if (!ended) return invalidArchive();
|
|
3474
|
+
return {
|
|
3475
|
+
success: true,
|
|
3476
|
+
value: files
|
|
3477
|
+
};
|
|
3478
|
+
}
|
|
3479
|
+
function readTarPath(header) {
|
|
3480
|
+
const name = readTarString(header.subarray(0, 100));
|
|
3481
|
+
const prefix = readTarString(header.subarray(345, 500));
|
|
3482
|
+
if (name === null || prefix === null || name.length === 0) return verificationError("BUNDLE_INVALID_PATH", "The plugin bundle contains a malformed path.");
|
|
3483
|
+
return {
|
|
3484
|
+
success: true,
|
|
3485
|
+
value: prefix ? `${prefix}/${name}` : name
|
|
3486
|
+
};
|
|
3487
|
+
}
|
|
3488
|
+
function readTarString(field) {
|
|
3489
|
+
const terminator = field.indexOf(0);
|
|
3490
|
+
const end = terminator === -1 ? field.length : terminator;
|
|
3491
|
+
if (terminator !== -1 && field.subarray(terminator + 1).some((byte) => byte !== 0)) return null;
|
|
3492
|
+
try {
|
|
3493
|
+
return decoder.decode(field.subarray(0, end));
|
|
3494
|
+
} catch {
|
|
3495
|
+
return null;
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
function normalizePath(raw, isDirectory) {
|
|
3499
|
+
if (raw.includes("\\") || raw.startsWith("/") || WINDOWS_DRIVE_PATTERN.test(raw) || hasControlCharacter(raw) || raw.endsWith("/") !== isDirectory) return verificationError("BUNDLE_INVALID_PATH", "The plugin bundle contains an unsafe path.");
|
|
3500
|
+
const parts = raw.split("/");
|
|
3501
|
+
if (isDirectory) parts.pop();
|
|
3502
|
+
const normalized = [];
|
|
3503
|
+
for (const part of parts) {
|
|
3504
|
+
if (part === ".") continue;
|
|
3505
|
+
if (part === "" || part === "..") return verificationError("BUNDLE_INVALID_PATH", "The plugin bundle contains an unsafe path.");
|
|
3506
|
+
normalized.push(part.normalize("NFC"));
|
|
3507
|
+
}
|
|
3508
|
+
if (normalized.length === 0) return verificationError("BUNDLE_INVALID_PATH", "The plugin bundle contains a malformed path.");
|
|
3509
|
+
return {
|
|
3510
|
+
success: true,
|
|
3511
|
+
value: normalized.join("/")
|
|
3512
|
+
};
|
|
3513
|
+
}
|
|
3514
|
+
function hasControlCharacter(value) {
|
|
3515
|
+
for (const character of value) {
|
|
3516
|
+
const codePoint = character.codePointAt(0);
|
|
3517
|
+
if (codePoint !== void 0 && (codePoint <= 31 || codePoint === 127)) return true;
|
|
3518
|
+
}
|
|
3519
|
+
return false;
|
|
3520
|
+
}
|
|
3521
|
+
function readOctal(field) {
|
|
3522
|
+
const text = new TextDecoder().decode(field).replaceAll("\0", "").trim();
|
|
3523
|
+
if (!OCTAL_PATTERN.test(text)) return null;
|
|
3524
|
+
const value = Number.parseInt(text, 8);
|
|
3525
|
+
return Number.isSafeInteger(value) ? value : null;
|
|
3526
|
+
}
|
|
3527
|
+
function validChecksum(header) {
|
|
3528
|
+
const expected = readOctal(header.subarray(148, 156));
|
|
3529
|
+
if (expected === null) return false;
|
|
3530
|
+
let actual = 0;
|
|
3531
|
+
for (let index = 0; index < header.length; index += 1) actual += index >= 148 && index < 156 ? 32 : header[index] ?? 0;
|
|
3532
|
+
return actual === expected;
|
|
3533
|
+
}
|
|
3534
|
+
function isZeroBlock(block) {
|
|
3535
|
+
return block.byteLength === TAR_BLOCK_BYTES && block.every((byte) => byte === 0);
|
|
3536
|
+
}
|
|
3537
|
+
function invalidArchive() {
|
|
3538
|
+
return verificationError("BUNDLE_INVALID_ARCHIVE", "The plugin bundle is not a valid tar archive.");
|
|
3539
|
+
}
|
|
3540
|
+
|
|
3541
|
+
//#endregion
|
|
3542
|
+
//#region src/registry/artifact-verification.ts
|
|
3543
|
+
const bundleDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
3544
|
+
async function validateRegistryArtifact(bytes, checksum, slug, version) {
|
|
3545
|
+
const checksumReport = await verifyMultihash(bytes, checksum);
|
|
3546
|
+
if (!checksumReport.success) return checksumReport;
|
|
3547
|
+
const [artifactDigest, ...artifactDigests] = await computeArtifactDigestCandidates(bytes);
|
|
3548
|
+
const bundleReport = await validatePluginBundle(bytes, {
|
|
3549
|
+
expectedSlug: slug,
|
|
3550
|
+
expectedVersion: version
|
|
3551
|
+
});
|
|
3552
|
+
if (!bundleReport.success) return bundleReport;
|
|
3553
|
+
const manifest = pluginManifestSchema$1.safeParse(bundleReport.value.manifest);
|
|
3554
|
+
if (!manifest.success) return {
|
|
3555
|
+
success: false,
|
|
3556
|
+
error: {
|
|
3557
|
+
code: "BUNDLE_RUNTIME_UNSUPPORTED",
|
|
3558
|
+
message: "The plugin bundle uses manifest features unsupported by this EmDash version."
|
|
3559
|
+
}
|
|
3560
|
+
};
|
|
3561
|
+
try {
|
|
3562
|
+
return {
|
|
3563
|
+
success: true,
|
|
3564
|
+
value: {
|
|
3565
|
+
bundle: {
|
|
3566
|
+
manifest: reconcileManifestAccess$1(manifest.data),
|
|
3567
|
+
backendCode: bundleDecoder.decode(bundleReport.value.backend),
|
|
3568
|
+
adminCode: bundleReport.value.admin === void 0 ? void 0 : bundleDecoder.decode(bundleReport.value.admin),
|
|
3569
|
+
checksum
|
|
3570
|
+
},
|
|
3571
|
+
artifactDigest,
|
|
3572
|
+
artifactDigests
|
|
3573
|
+
}
|
|
3574
|
+
};
|
|
3575
|
+
} catch {
|
|
3576
|
+
return {
|
|
3577
|
+
success: false,
|
|
3578
|
+
error: {
|
|
3579
|
+
code: "BUNDLE_INVALID_CODE_ENCODING",
|
|
3580
|
+
message: "Plugin bundle code must be valid UTF-8."
|
|
3581
|
+
}
|
|
3582
|
+
};
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
|
|
3586
|
+
//#endregion
|
|
3587
|
+
//#region src/registry/plugin-id.ts
|
|
3588
|
+
/**
|
|
3589
|
+
* Plugin identifier helpers for the experimental decentralized plugin
|
|
3590
|
+
* registry.
|
|
3591
|
+
*
|
|
3592
|
+
* Registry plugins are addressed by `(publisher_did, slug)`, but the
|
|
3593
|
+
* EmDash runtime threads a single `pluginId: string` through every
|
|
3594
|
+
* install primitive (R2 storage keys, `PluginStateRepository`,
|
|
3595
|
+
* `syncMarketplacePlugins`, sandbox cache keys). Rather than refactor
|
|
3596
|
+
* everything to carry a composite identifier, we normalize the registry
|
|
3597
|
+
* tuple to an opaque content-addressed id that satisfies the existing
|
|
3598
|
+
* `validatePluginIdentifier` shape (`/^[a-z][a-z0-9_-]*$/`).
|
|
3599
|
+
*
|
|
3600
|
+
* The normalized id is:
|
|
3601
|
+
*
|
|
3602
|
+
* `r_` + base32-encoded SHA-256(publisher_did + "\n" + slug), truncated.
|
|
3603
|
+
*
|
|
3604
|
+
* Properties:
|
|
3605
|
+
*
|
|
3606
|
+
* - Deterministic. The same `(publisher, slug)` always produces the
|
|
3607
|
+
* same id, so re-resolving an installed plugin's metadata against
|
|
3608
|
+
* the aggregator is a straightforward lookup keyed by the columns
|
|
3609
|
+
* stored alongside `plugin_id` in `plugin_states`.
|
|
3610
|
+
* - Collision-resistant. 80 bits of truncated hash; a 50% birthday
|
|
3611
|
+
* collision happens around 2^40 distinct plugins, well beyond what
|
|
3612
|
+
* this registry will ever index.
|
|
3613
|
+
* - R2-safe. Lowercase alphanumerics + underscore (no hyphens), no
|
|
3614
|
+
* `:` or `/`. Existing sandbox cache keys (`${pluginId}:${version}`)
|
|
3615
|
+
* keep working because the id contains no `:`.
|
|
3616
|
+
* - Syntactically distinct from typical marketplace plugin ids: the
|
|
3617
|
+
* `r_` prefix plus exactly 16 base32 characters is unlikely to be
|
|
3618
|
+
* chosen as a marketplace id. Not formally guaranteed by the
|
|
3619
|
+
* validator -- marketplace ids may begin with `r_` and contain
|
|
3620
|
+
* hyphens -- so the install handler also performs an explicit
|
|
3621
|
+
* pre-existing-row check at the derived id and rejects any cross-
|
|
3622
|
+
* source collision (`PLUGIN_ID_COLLISION`).
|
|
3623
|
+
*
|
|
3624
|
+
* Reverse lookup (id → publisher + slug) requires the `plugin_states`
|
|
3625
|
+
* row -- the hash is one-way. That's intentional: any code path that
|
|
3626
|
+
* needs the human-meaningful pair already has the state row in hand.
|
|
3627
|
+
*/
|
|
3628
|
+
/** Length (in base32 characters) of the truncated hash portion of the id. */
|
|
3629
|
+
const HASH_LENGTH = 16;
|
|
3630
|
+
/** Total expected length of a registry plugin id. */
|
|
3631
|
+
const REGISTRY_PLUGIN_ID_LENGTH = 2 + HASH_LENGTH;
|
|
3632
|
+
const BASE32_ALPHABET = "abcdefghijklmnopqrstuvwxyz234567";
|
|
3633
|
+
/**
|
|
3634
|
+
* RFC 4648 base32 encoding without padding, lowercase. Implemented inline
|
|
3635
|
+
* rather than depending on a multibase library because (a) we only need
|
|
3636
|
+
* lowercase base32 here, (b) we need it to run identically in workerd,
|
|
3637
|
+
* Node, and the browser, and (c) the implementation is fewer lines than
|
|
3638
|
+
* the import statement would be.
|
|
3639
|
+
*/
|
|
3640
|
+
function base32Encode(bytes) {
|
|
3641
|
+
let bits = 0;
|
|
3642
|
+
let value = 0;
|
|
3643
|
+
let out = "";
|
|
3644
|
+
for (const byte of bytes) {
|
|
3645
|
+
value = value << 8 | byte;
|
|
3646
|
+
bits += 8;
|
|
3647
|
+
while (bits >= 5) {
|
|
3648
|
+
bits -= 5;
|
|
3649
|
+
out += BASE32_ALPHABET[value >>> bits & 31];
|
|
3650
|
+
}
|
|
3651
|
+
}
|
|
3652
|
+
if (bits > 0) out += BASE32_ALPHABET[value << 5 - bits & 31];
|
|
3653
|
+
return out;
|
|
3654
|
+
}
|
|
3655
|
+
/**
|
|
3656
|
+
* Derive the normalized plugin id for a registry-published plugin.
|
|
3657
|
+
*
|
|
3658
|
+
* Throws if either input is empty or whitespace-only -- a missing DID
|
|
3659
|
+
* or slug is always a programming error in the install path, not a
|
|
3660
|
+
* recoverable runtime condition.
|
|
3661
|
+
*/
|
|
3662
|
+
async function makeRegistryPluginId(publisherDid, slug) {
|
|
3663
|
+
const did = publisherDid.trim();
|
|
3664
|
+
const s = slug.trim();
|
|
3665
|
+
if (!did) throw new Error("makeRegistryPluginId: publisherDid is required");
|
|
3666
|
+
if (!s) throw new Error("makeRegistryPluginId: slug is required");
|
|
3667
|
+
const input = `${did}\n${s}`;
|
|
3668
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input));
|
|
3669
|
+
return `r_${base32Encode(new Uint8Array(hashBuffer)).slice(0, HASH_LENGTH)}`;
|
|
3670
|
+
}
|
|
3671
|
+
|
|
3672
|
+
//#endregion
|
|
3673
|
+
//#region src/registry/record-labels.ts
|
|
3674
|
+
function hasCurrentRecordLabel(labels, value, record, now = Date.now()) {
|
|
3675
|
+
return labels.some((label) => {
|
|
3676
|
+
if (label.val !== value || label.uri !== record.uri || label.cid !== record.cid || label.neg === true) return false;
|
|
3677
|
+
if (label.exp === void 0) return true;
|
|
3678
|
+
const expiresAt = Date.parse(label.exp);
|
|
3679
|
+
return Number.isFinite(expiresAt) && expiresAt > now;
|
|
3680
|
+
});
|
|
3681
|
+
}
|
|
3682
|
+
|
|
3683
|
+
//#endregion
|
|
3684
|
+
//#region src/api/handlers/registry.ts
|
|
3685
|
+
/**
|
|
3686
|
+
* Registry installs use aggregator results only for discovery and moderation
|
|
3687
|
+
* metadata. The handler reads profile and release records from the publisher's
|
|
3688
|
+
* PDS, verifies their repository proofs, then validates the bundle checksum,
|
|
3689
|
+
* manifest, policy, and consent before storing or activating executable code.
|
|
3690
|
+
* Applicable labels can block installation but cannot supply any verified
|
|
3691
|
+
* record, checksum, permission, or executable byte.
|
|
3692
|
+
*/
|
|
3693
|
+
function verifiedAccessEqual(a, b) {
|
|
3694
|
+
return JSON.stringify(a) === JSON.stringify(canonicalizeDeclaredAccess(b));
|
|
3695
|
+
}
|
|
3696
|
+
function registryArtifactError(code, message, operation) {
|
|
3697
|
+
let apiCode;
|
|
3698
|
+
switch (code) {
|
|
3699
|
+
case "BUNDLE_ID_MISMATCH":
|
|
3700
|
+
apiCode = operation === "install" ? "MANIFEST_ID_MISMATCH" : "BUNDLE_IDENTITY_MISMATCH";
|
|
3701
|
+
break;
|
|
3702
|
+
case "BUNDLE_VERSION_MISMATCH":
|
|
3703
|
+
apiCode = operation === "install" ? "MANIFEST_VERSION_MISMATCH" : code;
|
|
3704
|
+
break;
|
|
3705
|
+
case "CHECKSUM_MISMATCH":
|
|
3706
|
+
case "INVALID_MULTIHASH":
|
|
3707
|
+
case "UNSUPPORTED_MULTIHASH":
|
|
3708
|
+
apiCode = "CHECKSUM_MISMATCH";
|
|
3709
|
+
break;
|
|
3710
|
+
case "FETCH_FAILED":
|
|
3711
|
+
case "HOST_REJECTED":
|
|
3712
|
+
case "REDIRECT_LIMIT_EXCEEDED":
|
|
3713
|
+
case "REDIRECT_LOCATION_MISSING":
|
|
3714
|
+
case "RESOURCE_STATUS_ERROR":
|
|
3715
|
+
case "RESOURCE_TIMEOUT":
|
|
3716
|
+
apiCode = operation === "install" ? "INSTALL_FAILED" : "UPDATE_FAILED";
|
|
3717
|
+
break;
|
|
3718
|
+
default: apiCode = "INVALID_BUNDLE";
|
|
3719
|
+
}
|
|
3720
|
+
return {
|
|
3721
|
+
success: false,
|
|
3722
|
+
error: {
|
|
3723
|
+
code: apiCode,
|
|
3724
|
+
message,
|
|
3725
|
+
details: { verificationCode: code }
|
|
3726
|
+
}
|
|
3727
|
+
};
|
|
3728
|
+
}
|
|
3729
|
+
function registryRecordError(code, message) {
|
|
3730
|
+
return {
|
|
3731
|
+
success: false,
|
|
3732
|
+
error: {
|
|
3733
|
+
code: "RECORD_VERIFICATION_FAILED",
|
|
3734
|
+
message,
|
|
3735
|
+
details: { verificationCode: code }
|
|
3736
|
+
}
|
|
3737
|
+
};
|
|
3738
|
+
}
|
|
3739
|
+
function recordConsentError(input, records) {
|
|
3740
|
+
if (!input.profileCid || !input.releaseCid) return {
|
|
3741
|
+
success: false,
|
|
3742
|
+
error: {
|
|
3743
|
+
code: "RECORD_CONSENT_REQUIRED",
|
|
3744
|
+
message: "Verify the signed package records before confirming this action."
|
|
3745
|
+
}
|
|
3746
|
+
};
|
|
3747
|
+
if (input.profileCid !== records.profile.cid || input.releaseCid !== records.release.cid) return {
|
|
3748
|
+
success: false,
|
|
3749
|
+
error: {
|
|
3750
|
+
code: "RECORD_VERIFICATION_DRIFT",
|
|
3751
|
+
message: "The signed package records changed after review. Verify them again."
|
|
3752
|
+
}
|
|
3753
|
+
};
|
|
3754
|
+
return null;
|
|
3755
|
+
}
|
|
3756
|
+
function recordVerificationSummary(records, report) {
|
|
3757
|
+
return {
|
|
3758
|
+
profileCid: records.profile.cid,
|
|
3759
|
+
releaseCid: records.release.cid,
|
|
3760
|
+
provenance: report.provenance.status,
|
|
3761
|
+
policy: report.value.policy
|
|
3762
|
+
};
|
|
3763
|
+
}
|
|
3764
|
+
/**
|
|
3765
|
+
* Bytes-per-artifact cap on the gzipped tarball we'll download before
|
|
3766
|
+
* decompression. RFC 0001 caps a sandboxed plugin bundle at 256 KiB
|
|
3767
|
+
* decompressed (see `MAX_BUNDLE_SIZE` in cli/commands/bundle-utils.ts);
|
|
3768
|
+
* gzip on a mix of JSON manifest + JS code typically gives 0.3-0.6
|
|
3769
|
+
* ratio, so compressed bundles are well under 200 KiB in practice.
|
|
3770
|
+
* 512 KiB leaves margin for unusual file mixes that compress poorly
|
|
3771
|
+
* while still rejecting anything that's obviously not a legitimate
|
|
3772
|
+
* plugin bundle.
|
|
3773
|
+
*/
|
|
3774
|
+
const MAX_ARTIFACT_BYTES = 512 * 1024;
|
|
3775
|
+
/**
|
|
3776
|
+
* Maximum number of HTTP redirects followed during artifact download.
|
|
3777
|
+
* Each hop is independently URL-validated, so a malicious server cannot
|
|
3778
|
+
* redirect through a series of allowed-looking origins to reach a
|
|
3779
|
+
* forbidden one.
|
|
3780
|
+
*/
|
|
3781
|
+
const MAX_REDIRECTS = 5;
|
|
3782
|
+
/**
|
|
3783
|
+
* Wall-clock cap on any single artifact fetch attempt (per URL).
|
|
3784
|
+
* Defends against slow-loris artifact sources that accept the connection but
|
|
3785
|
+
* never finish sending headers or body.
|
|
3786
|
+
*/
|
|
3787
|
+
const ARTIFACT_FETCH_TIMEOUT_MS = 15e3;
|
|
3788
|
+
/**
|
|
3789
|
+
* Total wall-clock budget for the artifact-download phase across all
|
|
3790
|
+
* advertised caches, the publisher PDS, and the declared URL. Even with the
|
|
3791
|
+
* per-URL timeout, unavailable sources could otherwise tie up the install request for
|
|
3792
|
+
* minutes; this caps total time at a budget interactive admins can
|
|
3793
|
+
* tolerate. Tuned so a fast happy path takes <1s of budget per
|
|
3794
|
+
* attempt and a worst case still completes in under a minute.
|
|
3795
|
+
*/
|
|
3796
|
+
const ARTIFACT_TOTAL_BUDGET_MS = 45e3;
|
|
3797
|
+
/**
|
|
3798
|
+
* Per-request timeout applied to every aggregator XRPC call
|
|
3799
|
+
* (`resolvePackage`, `getLatestRelease`, `listReleases`). Matches the
|
|
3800
|
+
* per-URL artifact-fetch cap. Without this, a slow-loris aggregator
|
|
3801
|
+
* can stall the install before the artifact phase even starts.
|
|
3802
|
+
*/
|
|
3803
|
+
const AGGREGATOR_REQUEST_TIMEOUT_MS = 15e3;
|
|
3804
|
+
/**
|
|
3805
|
+
* Total wall-clock budget for the aggregator-discovery phase
|
|
3806
|
+
* (resolve + selected-release lookup). Mirrors the artifact-download
|
|
3807
|
+
* budget. Worst case with the pinned-version path's 20-page cap is
|
|
3808
|
+
* 20 + 1 calls; capping the total ensures any one stalled call
|
|
3809
|
+
* still bounds the whole phase.
|
|
3810
|
+
*/
|
|
3811
|
+
const AGGREGATOR_TOTAL_BUDGET_MS = 3e4;
|
|
3812
|
+
var RegistryArtifactFetchError = class extends Error {
|
|
3813
|
+
constructor(code, message) {
|
|
3814
|
+
super(message);
|
|
3815
|
+
this.code = code;
|
|
3816
|
+
this.name = "RegistryArtifactFetchError";
|
|
3817
|
+
}
|
|
3818
|
+
};
|
|
3819
|
+
/** Build a fetch function that enforces a per-request and per-budget timeout. */
|
|
3820
|
+
function timedFetch(totalDeadline) {
|
|
3821
|
+
return (input, init) => {
|
|
3822
|
+
const now = Date.now();
|
|
3823
|
+
const remaining = Math.max(0, totalDeadline - now);
|
|
3824
|
+
if (remaining === 0) return Promise.reject(/* @__PURE__ */ new Error("Aggregator request budget exhausted"));
|
|
3825
|
+
const timeout = Math.min(AGGREGATOR_REQUEST_TIMEOUT_MS, remaining);
|
|
3826
|
+
const controller = new AbortController();
|
|
3827
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
3828
|
+
const callerSignal = init?.signal;
|
|
3829
|
+
if (callerSignal) if (callerSignal.aborted) controller.abort(callerSignal.reason);
|
|
3830
|
+
else callerSignal.addEventListener("abort", () => controller.abort(callerSignal.reason));
|
|
3831
|
+
return fetch(input, {
|
|
3832
|
+
...init,
|
|
3833
|
+
signal: controller.signal
|
|
3834
|
+
}).finally(() => {
|
|
3835
|
+
clearTimeout(timer);
|
|
3836
|
+
});
|
|
3837
|
+
};
|
|
3838
|
+
}
|
|
3839
|
+
async function fetchArtifact(artifactCaches, artifact, record, auth) {
|
|
3840
|
+
const result = await fetchReleaseArtifact({
|
|
3841
|
+
artifact,
|
|
3842
|
+
record,
|
|
3843
|
+
artifactCaches,
|
|
3844
|
+
...auth === void 0 ? {} : { auth }
|
|
3845
|
+
}, {
|
|
3846
|
+
fetch: async (url, init) => {
|
|
3847
|
+
if (!init.signal) throw new Error("Registry artifact fetch requires an abort signal");
|
|
3848
|
+
return fetchRegistryArtifactUrl(url.href, {
|
|
3849
|
+
signal: init.signal,
|
|
3850
|
+
maxResponseBytes: MAX_ARTIFACT_BYTES
|
|
3851
|
+
});
|
|
3852
|
+
},
|
|
3853
|
+
resolveHostname: async (hostname) => (await resolveAndValidateExternalUrlTarget(`https://${hostname}`)).addresses,
|
|
3854
|
+
allowHttpLocalhost: import.meta.env.DEV,
|
|
3855
|
+
headerTimeoutMs: ARTIFACT_FETCH_TIMEOUT_MS,
|
|
3856
|
+
totalTimeoutMs: ARTIFACT_TOTAL_BUDGET_MS,
|
|
3857
|
+
maxBytes: MAX_ARTIFACT_BYTES,
|
|
3858
|
+
maxRedirects: MAX_REDIRECTS
|
|
3859
|
+
});
|
|
3860
|
+
if (!result.success) throw new RegistryArtifactFetchError(result.error.code, result.error.message);
|
|
3861
|
+
return result.value.bytes;
|
|
3862
|
+
}
|
|
3863
|
+
/**
|
|
3864
|
+
* Gate a release's `requires` constraints against the running host
|
|
3865
|
+
* environment. `requires` is the lexicon-`unknown` value off the signed
|
|
3866
|
+
* release record — never trust its shape; `checkEnvCompatibility` guards it.
|
|
3867
|
+
*
|
|
3868
|
+
* Returns `null` when every advertised constraint is satisfied (or there are
|
|
3869
|
+
* none), or a structured `ENV_INCOMPATIBLE` error naming the unsatisfied
|
|
3870
|
+
* constraints and the host versions. The error carries the guarded `requires`
|
|
3871
|
+
* and `host` maps so the admin can render the same mismatch the UI gate shows.
|
|
3872
|
+
*/
|
|
3873
|
+
function assertEnvCompatible(requires, hostEnv) {
|
|
3874
|
+
for (const skipped of findSkippedEnvConstraints(requires, hostEnv)) console.warn(`[registry] env compatibility constraint skipped: ${skipped.key} requires ${skipped.required} but host version is ${skipped.reason}`);
|
|
3875
|
+
const mismatches = checkEnvCompatibility(requires, hostEnv);
|
|
3876
|
+
if (mismatches.length === 0) return null;
|
|
3877
|
+
const guarded = {};
|
|
3878
|
+
for (const m of mismatches) guarded[m.key] = m.required;
|
|
3879
|
+
return {
|
|
3880
|
+
code: "ENV_INCOMPATIBLE",
|
|
3881
|
+
message: `This release is not compatible with the current environment: ${mismatches.map((m) => `${m.key} requires ${m.required} but host is ${m.host}`).join("; ")}.`,
|
|
3882
|
+
details: {
|
|
3883
|
+
requires: guarded,
|
|
3884
|
+
host: hostEnv
|
|
3885
|
+
}
|
|
3886
|
+
};
|
|
3887
|
+
}
|
|
3888
|
+
async function handleRegistryInstall(db, storage, sandboxRunner, registryConfigInput, input, opts) {
|
|
3889
|
+
const registryConfig = coerceRegistryConfig(registryConfigInput);
|
|
3890
|
+
if (!registryConfig) return {
|
|
3891
|
+
success: false,
|
|
3892
|
+
error: {
|
|
3893
|
+
code: "REGISTRY_NOT_CONFIGURED",
|
|
3894
|
+
message: "Registry is not configured"
|
|
3895
|
+
}
|
|
3896
|
+
};
|
|
3897
|
+
if (!storage) return {
|
|
3898
|
+
success: false,
|
|
3899
|
+
error: {
|
|
3900
|
+
code: "STORAGE_NOT_CONFIGURED",
|
|
3901
|
+
message: "Storage is required for registry plugin installation"
|
|
3902
|
+
}
|
|
3903
|
+
};
|
|
3904
|
+
if (!sandboxRunner || !sandboxRunner.isAvailable()) return {
|
|
3905
|
+
success: false,
|
|
3906
|
+
error: {
|
|
3907
|
+
code: "SANDBOX_NOT_AVAILABLE",
|
|
3908
|
+
message: withUnavailableReason("Sandbox runner is required for registry plugins", sandboxRunner)
|
|
3909
|
+
}
|
|
3910
|
+
};
|
|
3911
|
+
try {
|
|
3912
|
+
validateAggregatorUrl(registryConfig.aggregatorUrl);
|
|
3913
|
+
} catch (err) {
|
|
3914
|
+
return {
|
|
3915
|
+
success: false,
|
|
3916
|
+
error: {
|
|
3917
|
+
code: "REGISTRY_NOT_CONFIGURED",
|
|
3918
|
+
message: err instanceof Error ? err.message : "Invalid aggregator URL"
|
|
3919
|
+
}
|
|
3920
|
+
};
|
|
3921
|
+
}
|
|
3922
|
+
const { did, slug, version: requestedVersion } = input;
|
|
3923
|
+
const { DiscoveryClient, registryLabelerPolicy } = await import("@emdash-cms/registry-client/discovery");
|
|
3924
|
+
const aggregatorDeadline = Date.now() + AGGREGATOR_TOTAL_BUDGET_MS;
|
|
3925
|
+
const discovery = new DiscoveryClient({
|
|
3926
|
+
aggregatorUrl: registryConfig.aggregatorUrl,
|
|
3927
|
+
acceptLabelers: registryConfig.acceptLabelers,
|
|
3928
|
+
labelerPolicy: registryLabelerPolicy(registryConfig.acceptLabelers),
|
|
3929
|
+
fetch: timedFetch(aggregatorDeadline)
|
|
3930
|
+
});
|
|
3931
|
+
if (!did.startsWith("did:") || did.split(":").length < 3) return {
|
|
3932
|
+
success: false,
|
|
3933
|
+
error: {
|
|
3934
|
+
code: "INVALID_DID",
|
|
3935
|
+
message: "DID must be a valid atproto DID (e.g. did:plc:abc123)"
|
|
3936
|
+
}
|
|
3937
|
+
};
|
|
3938
|
+
try {
|
|
3939
|
+
const publisherDid = did;
|
|
3940
|
+
const packageView = await discovery.getPackage({
|
|
3941
|
+
did: publisherDid,
|
|
3942
|
+
slug
|
|
3943
|
+
});
|
|
3944
|
+
const MAX_LIST_PAGES = 20;
|
|
3945
|
+
const releaseView = await (async () => {
|
|
3946
|
+
if (!requestedVersion) return discovery.getLatestRelease({
|
|
3947
|
+
did: publisherDid,
|
|
3948
|
+
package: slug
|
|
3949
|
+
});
|
|
3950
|
+
let cursor;
|
|
3951
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
3952
|
+
for (let page = 0; page < MAX_LIST_PAGES; page++) {
|
|
3953
|
+
if (cursor !== void 0) {
|
|
3954
|
+
if (seenCursors.has(cursor)) break;
|
|
3955
|
+
seenCursors.add(cursor);
|
|
3956
|
+
}
|
|
3957
|
+
const result = await discovery.listReleases({
|
|
3958
|
+
did: publisherDid,
|
|
3959
|
+
package: slug,
|
|
3960
|
+
cursor,
|
|
3961
|
+
limit: 50
|
|
3962
|
+
});
|
|
3963
|
+
for (const r of result.releases) if (r.version === requestedVersion) return r;
|
|
3964
|
+
if (!result.cursor) break;
|
|
3965
|
+
cursor = result.cursor;
|
|
3966
|
+
}
|
|
3967
|
+
})();
|
|
3968
|
+
if (!releaseView) return {
|
|
3969
|
+
success: false,
|
|
3970
|
+
error: {
|
|
3971
|
+
code: "NO_RELEASE",
|
|
3972
|
+
message: requestedVersion ? `Version ${requestedVersion} not found for ${publisherDid}/${slug}` : `No installable release found for ${publisherDid}/${slug}`
|
|
3973
|
+
}
|
|
3974
|
+
};
|
|
3975
|
+
if (packageView.did !== publisherDid || packageView.slug !== slug) return {
|
|
3976
|
+
success: false,
|
|
3977
|
+
error: {
|
|
3978
|
+
code: "AGGREGATOR_IDENTITY_MISMATCH",
|
|
3979
|
+
message: "Aggregator returned a package view for a different publisher or slug."
|
|
3980
|
+
}
|
|
3981
|
+
};
|
|
3982
|
+
if (releaseView.did !== publisherDid || releaseView.package !== slug || requestedVersion !== void 0 && releaseView.version !== requestedVersion) return {
|
|
3983
|
+
success: false,
|
|
3984
|
+
error: {
|
|
3985
|
+
code: "AGGREGATOR_IDENTITY_MISMATCH",
|
|
3986
|
+
message: "Aggregator returned a release view that does not match the requested package or version."
|
|
3987
|
+
}
|
|
3988
|
+
};
|
|
3989
|
+
const version = releaseView.version;
|
|
3990
|
+
if (evaluateRegistryReleaseWithdrawal(releaseView, discovery.labelerPolicy).withdrawn) return {
|
|
3991
|
+
success: false,
|
|
3992
|
+
error: {
|
|
3993
|
+
code: "RELEASE_YANKED",
|
|
3994
|
+
message: "This release has been withdrawn"
|
|
3995
|
+
}
|
|
3996
|
+
};
|
|
3997
|
+
const authoritative = await (opts?.readAuthoritativeRecords ?? readAuthoritativePackageRelease)(publisherDid, slug, version, opts?.authoritativeRecords);
|
|
3998
|
+
if (!authoritative.success) return registryRecordError(authoritative.error.code, authoritative.error.message);
|
|
3999
|
+
const records = authoritative.value;
|
|
4000
|
+
const { profile, release } = records.inspection.value;
|
|
4001
|
+
if (packageView.uri !== records.profile.uri || packageView.cid !== records.profile.cid || releaseView.uri !== records.release.uri || releaseView.cid !== records.release.cid) return {
|
|
4002
|
+
success: false,
|
|
4003
|
+
error: {
|
|
4004
|
+
code: "AGGREGATOR_RECORD_MISMATCH",
|
|
4005
|
+
message: "Aggregator metadata does not match the publisher's signed records."
|
|
4006
|
+
}
|
|
4007
|
+
};
|
|
4008
|
+
if (!opts?.verifyOnly) {
|
|
4009
|
+
const consentError = recordConsentError({
|
|
4010
|
+
profileCid: input.acknowledgedProfileCid,
|
|
4011
|
+
releaseCid: input.acknowledgedReleaseCid
|
|
4012
|
+
}, records);
|
|
4013
|
+
if (consentError) return consentError;
|
|
4014
|
+
}
|
|
4015
|
+
if (hasCurrentRecordLabel(packageView.labels ?? [], "security:yanked", records.profile) || hasCurrentRecordLabel(packageView.labels ?? [], "security-yanked", records.profile)) return {
|
|
4016
|
+
success: false,
|
|
4017
|
+
error: {
|
|
4018
|
+
code: "RELEASE_YANKED",
|
|
4019
|
+
message: "This release has been withdrawn"
|
|
4020
|
+
}
|
|
4021
|
+
};
|
|
4022
|
+
if (opts?.hostEnv) {
|
|
4023
|
+
const envError = assertEnvCompatible(release.requires, opts.hostEnv);
|
|
4024
|
+
if (envError) return {
|
|
4025
|
+
success: false,
|
|
4026
|
+
error: envError
|
|
4027
|
+
};
|
|
4028
|
+
}
|
|
4029
|
+
const minimumReleaseAge = registryConfig.policy?.minimumReleaseAge;
|
|
4030
|
+
let minimumReleaseAgeSeconds = 0;
|
|
4031
|
+
if (minimumReleaseAge !== void 0) try {
|
|
4032
|
+
minimumReleaseAgeSeconds = parseDurationSeconds(minimumReleaseAge);
|
|
4033
|
+
} catch (err) {
|
|
4034
|
+
return {
|
|
4035
|
+
success: false,
|
|
4036
|
+
error: {
|
|
4037
|
+
code: "REGISTRY_POLICY_INVALID",
|
|
4038
|
+
message: err instanceof Error ? err.message : "Invalid minimumReleaseAge value in registry config"
|
|
4039
|
+
}
|
|
4040
|
+
};
|
|
4041
|
+
}
|
|
4042
|
+
if (minimumReleaseAgeSeconds > 0) {
|
|
4043
|
+
const exclude = registryConfig.policy?.minimumReleaseAgeExclude?.map((e) => e.trim().toLowerCase());
|
|
4044
|
+
if (!(releaseExemptFromMinimumAge(exclude, publisherDid, slug) || isProvenFirstRelease(packageView))) {
|
|
4045
|
+
const indexedAt = Date.parse(releaseView.indexedAt);
|
|
4046
|
+
if (!Number.isFinite(indexedAt)) return {
|
|
4047
|
+
success: false,
|
|
4048
|
+
error: {
|
|
4049
|
+
code: "RELEASE_TIMESTAMP_INVALID",
|
|
4050
|
+
message: "Release record is missing a valid indexed-at timestamp; cannot evaluate minimum release age policy."
|
|
4051
|
+
}
|
|
4052
|
+
};
|
|
4053
|
+
const ageSeconds = (Date.now() - indexedAt) / 1e3;
|
|
4054
|
+
if (ageSeconds < minimumReleaseAgeSeconds) {
|
|
4055
|
+
const remaining = Math.ceil(minimumReleaseAgeSeconds - ageSeconds);
|
|
4056
|
+
return {
|
|
4057
|
+
success: false,
|
|
4058
|
+
error: {
|
|
4059
|
+
code: "RELEASE_TOO_NEW",
|
|
4060
|
+
message: `This release does not meet the configured minimum release age of ${minimumReleaseAgeSeconds}s. It will be installable in ~${remaining}s.`
|
|
4061
|
+
}
|
|
4062
|
+
};
|
|
4063
|
+
}
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
4066
|
+
const pluginId = await makeRegistryPluginId(publisherDid, slug);
|
|
4067
|
+
if (opts?.configuredPluginIds?.has(pluginId)) return {
|
|
4068
|
+
success: false,
|
|
4069
|
+
error: {
|
|
4070
|
+
code: "PLUGIN_ID_CONFLICT",
|
|
4071
|
+
message: "A configured plugin with the same derived id already exists"
|
|
4072
|
+
}
|
|
4073
|
+
};
|
|
4074
|
+
const stateRepo = new PluginStateRepository(db);
|
|
4075
|
+
const existing = await stateRepo.get(pluginId);
|
|
4076
|
+
if (existing) {
|
|
4077
|
+
if (existing.source === "registry") return {
|
|
4078
|
+
success: false,
|
|
4079
|
+
error: {
|
|
4080
|
+
code: "ALREADY_INSTALLED",
|
|
4081
|
+
message: `Plugin ${publisherDid}/${slug} is already installed`
|
|
4082
|
+
}
|
|
4083
|
+
};
|
|
4084
|
+
return {
|
|
4085
|
+
success: false,
|
|
4086
|
+
error: {
|
|
4087
|
+
code: "PLUGIN_ID_COLLISION",
|
|
4088
|
+
message: `A non-registry plugin already exists at the derived id ${pluginId}. Uninstall it before installing this registry plugin.`
|
|
4089
|
+
}
|
|
4090
|
+
};
|
|
4091
|
+
}
|
|
4092
|
+
const packageArtifact = release.artifacts.package;
|
|
4093
|
+
const declaredChecksum = release.artifacts.package.checksum;
|
|
4094
|
+
if (!packageArtifact || !declaredChecksum) return {
|
|
4095
|
+
success: false,
|
|
4096
|
+
error: {
|
|
4097
|
+
code: "INVALID_RELEASE",
|
|
4098
|
+
message: "Release record is missing its package artifact or checksum"
|
|
4099
|
+
}
|
|
4100
|
+
};
|
|
4101
|
+
let artifactBytes;
|
|
4102
|
+
try {
|
|
4103
|
+
artifactBytes = await fetchArtifact(releaseView.artifactCaches ?? [], packageArtifact, {
|
|
4104
|
+
did: publisherDid,
|
|
4105
|
+
collection: NSID.packageRelease,
|
|
4106
|
+
rkey: `${slug}:${version}`,
|
|
4107
|
+
cid: releaseView.cid
|
|
4108
|
+
}, release.auth);
|
|
4109
|
+
} catch (error) {
|
|
4110
|
+
if (error instanceof RegistryArtifactFetchError) return registryArtifactError(error.code, error.message, "install");
|
|
4111
|
+
throw error;
|
|
4112
|
+
}
|
|
4113
|
+
const artifactReport = await validateRegistryArtifact(artifactBytes, declaredChecksum, slug, version);
|
|
4114
|
+
if (!artifactReport.success) return registryArtifactError(artifactReport.error.code, artifactReport.error.message, "install");
|
|
4115
|
+
const { bundle, artifactDigest, artifactDigests } = artifactReport.value;
|
|
4116
|
+
const recordReport = await verifyAuthoritativePackageRelease(records, artifactDigest, {
|
|
4117
|
+
...opts?.authoritativeRecords,
|
|
4118
|
+
artifactDigests
|
|
4119
|
+
});
|
|
4120
|
+
if (!recordReport.success) return registryRecordError(recordReport.code, recordReport.reasons[0]?.message ?? "The release provenance is invalid.");
|
|
4121
|
+
const verification = recordVerificationSummary(records, recordReport);
|
|
4122
|
+
bundle.manifest = {
|
|
4123
|
+
...bundle.manifest,
|
|
4124
|
+
id: pluginId
|
|
4125
|
+
};
|
|
4126
|
+
if (!verifiedAccessEqual(recordReport.value.declaredAccess, bundle.manifest.declaredAccess ?? {})) return {
|
|
4127
|
+
success: false,
|
|
4128
|
+
error: {
|
|
4129
|
+
code: "DECLARED_ACCESS_DRIFT",
|
|
4130
|
+
message: "The plugin bundle declares different permissions than its published record. Installation refused."
|
|
4131
|
+
}
|
|
4132
|
+
};
|
|
4133
|
+
const actualCapabilities = canonicalCapabilitiesForDriftCheck(bundle.manifest.capabilities);
|
|
4134
|
+
if (!opts?.verifyOnly && actualCapabilities.length > 0) {
|
|
4135
|
+
if (input.acknowledgedDeclaredAccess === void 0) return {
|
|
4136
|
+
success: false,
|
|
4137
|
+
error: {
|
|
4138
|
+
code: "DECLARED_ACCESS_REQUIRED",
|
|
4139
|
+
message: "This plugin declares capabilities that require consent. Re-open the install dialog to review and acknowledge them."
|
|
4140
|
+
}
|
|
4141
|
+
};
|
|
4142
|
+
const acknowledged = canonicalCapabilitiesForDriftCheck(input.acknowledgedDeclaredAccess);
|
|
4143
|
+
if (acknowledged.length !== actualCapabilities.length || acknowledged.some((cap, i) => cap !== actualCapabilities[i])) return {
|
|
4144
|
+
success: false,
|
|
4145
|
+
error: {
|
|
4146
|
+
code: "DECLARED_ACCESS_DRIFT",
|
|
4147
|
+
message: "Plugin manifest has changed since you consented. Re-open the install dialog to review the new permissions."
|
|
4148
|
+
}
|
|
4149
|
+
};
|
|
4150
|
+
}
|
|
4151
|
+
const actualMcpTools = (bundle.manifest.mcp?.tools ?? []).map(({ inputSchema: _, outputSchema: __, ...tool }) => tool);
|
|
4152
|
+
const publicRoutes = diffRouteVisibility(void 0, bundle.manifest).newlyPublic.toSorted();
|
|
4153
|
+
if (!opts?.verifyOnly && publicRoutes.length > 0) {
|
|
4154
|
+
const acknowledgedPublicRoutes = Array.isArray(input.acknowledgedPublicRoutes) ? input.acknowledgedPublicRoutes.filter((route) => typeof route === "string").toSorted() : [];
|
|
4155
|
+
if (JSON.stringify(acknowledgedPublicRoutes) !== JSON.stringify(publicRoutes)) return {
|
|
4156
|
+
success: false,
|
|
4157
|
+
error: {
|
|
4158
|
+
code: "ROUTE_VISIBILITY_ESCALATION",
|
|
4159
|
+
message: "Plugin install exposes public (unauthenticated) routes",
|
|
4160
|
+
details: {
|
|
4161
|
+
routeVisibilityChanges: { newlyPublic: publicRoutes },
|
|
4162
|
+
mcpTools: actualMcpTools,
|
|
4163
|
+
verification
|
|
4164
|
+
}
|
|
4165
|
+
}
|
|
4166
|
+
};
|
|
4167
|
+
}
|
|
4168
|
+
if (!opts?.verifyOnly && actualMcpTools.length > 0) {
|
|
4169
|
+
if (JSON.stringify(input.acknowledgedMcpTools) !== JSON.stringify(actualMcpTools)) return {
|
|
4170
|
+
success: false,
|
|
4171
|
+
error: {
|
|
4172
|
+
code: "MCP_TOOL_CONSENT_REQUIRED",
|
|
4173
|
+
message: "Plugin MCP tools require explicit consent",
|
|
4174
|
+
details: {
|
|
4175
|
+
mcpTools: actualMcpTools,
|
|
4176
|
+
routeVisibilityChanges: publicRoutes.length > 0 ? { newlyPublic: publicRoutes } : void 0,
|
|
4177
|
+
verification
|
|
4178
|
+
}
|
|
4179
|
+
}
|
|
4180
|
+
};
|
|
4181
|
+
}
|
|
4182
|
+
const result = {
|
|
4183
|
+
pluginId,
|
|
4184
|
+
publisherDid,
|
|
4185
|
+
slug,
|
|
4186
|
+
version,
|
|
4187
|
+
capabilities: bundle.manifest.capabilities,
|
|
4188
|
+
declaredAccess: recordReport.value.releaseExtension.declaredAccess,
|
|
4189
|
+
mcpTools: actualMcpTools,
|
|
4190
|
+
publicRoutes,
|
|
4191
|
+
verification
|
|
4192
|
+
};
|
|
4193
|
+
if (opts?.verifyOnly) return {
|
|
4194
|
+
success: true,
|
|
4195
|
+
data: result
|
|
4196
|
+
};
|
|
4197
|
+
await storeBundleInR2(storage, pluginId, version, bundle, "registry");
|
|
4198
|
+
try {
|
|
4199
|
+
await stateRepo.upsert(pluginId, version, "active", {
|
|
4200
|
+
source: "registry",
|
|
4201
|
+
displayName: profile.name ?? slug,
|
|
4202
|
+
description: profile.description ?? void 0,
|
|
4203
|
+
registryPublisherDid: publisherDid,
|
|
4204
|
+
registrySlug: slug
|
|
4205
|
+
});
|
|
4206
|
+
} catch (stateErr) {
|
|
4207
|
+
let lostRace = false;
|
|
4208
|
+
try {
|
|
4209
|
+
const winner = await stateRepo.get(pluginId);
|
|
4210
|
+
lostRace = winner !== void 0 && winner !== null;
|
|
4211
|
+
} catch (probeErr) {
|
|
4212
|
+
console.warn(`[registry-install] Failed to probe state row for ${pluginId} after state-write failure; treating as orphan:`, probeErr);
|
|
4213
|
+
}
|
|
4214
|
+
if (!lostRace) try {
|
|
4215
|
+
await deleteBundleFromR2(storage, pluginId, version, "registry");
|
|
4216
|
+
} catch (cleanupErr) {
|
|
4217
|
+
console.warn(`[registry-install] Failed to clean up R2 bundle for ${pluginId}@${version} after state-row write failure:`, cleanupErr);
|
|
4218
|
+
}
|
|
4219
|
+
throw stateErr;
|
|
4220
|
+
}
|
|
4221
|
+
await syncDeclaredStorageIndexes(db, [bundle.manifest]);
|
|
4222
|
+
return {
|
|
4223
|
+
success: true,
|
|
4224
|
+
data: result
|
|
4225
|
+
};
|
|
4226
|
+
} catch (err) {
|
|
4227
|
+
if (err instanceof ClientValidationError) return {
|
|
4228
|
+
success: false,
|
|
4229
|
+
error: {
|
|
4230
|
+
code: "AGGREGATOR_RESPONSE_INVALID",
|
|
4231
|
+
message: `Aggregator returned a response that does not conform to its lexicon (${err.target})`
|
|
4232
|
+
}
|
|
4233
|
+
};
|
|
4234
|
+
if (err instanceof ClientResponseError) {
|
|
4235
|
+
if (err.error === "ListingUnavailable") return {
|
|
4236
|
+
success: false,
|
|
4237
|
+
error: {
|
|
4238
|
+
code: "LISTING_UNAVAILABLE",
|
|
4239
|
+
message: "This plugin is unavailable under the active registry policy"
|
|
4240
|
+
}
|
|
4241
|
+
};
|
|
4242
|
+
return {
|
|
4243
|
+
success: false,
|
|
4244
|
+
error: {
|
|
4245
|
+
code: err.status === 404 ? "AGGREGATOR_NOT_FOUND" : "AGGREGATOR_HTTP_ERROR",
|
|
4246
|
+
message: `Aggregator returned ${err.status}: ${err.error}`
|
|
4247
|
+
}
|
|
4248
|
+
};
|
|
4249
|
+
}
|
|
4250
|
+
if (err instanceof EmDashStorageError) return {
|
|
4251
|
+
success: false,
|
|
4252
|
+
error: {
|
|
4253
|
+
code: err.code ?? "STORAGE_ERROR",
|
|
4254
|
+
message: "Storage error while installing plugin"
|
|
4255
|
+
}
|
|
4256
|
+
};
|
|
4257
|
+
console.error("[registry-install] Failed:", err);
|
|
4258
|
+
return {
|
|
4259
|
+
success: false,
|
|
4260
|
+
error: {
|
|
4261
|
+
code: "INSTALL_FAILED",
|
|
4262
|
+
message: err instanceof Error ? err.message : "Failed to install plugin from registry"
|
|
4263
|
+
}
|
|
4264
|
+
};
|
|
4265
|
+
}
|
|
4266
|
+
}
|
|
4267
|
+
/**
|
|
4268
|
+
* Uninstall a registry-source plugin. Deletes the R2 bundle under
|
|
4269
|
+
* `registry/<pluginId>/<version>/`, optionally drops the plugin's
|
|
4270
|
+
* `_plugin_storage` rows, and removes the `_plugin_state` row. The
|
|
4271
|
+
* sandbox runtime is reconciled by the route's `syncRegistryPlugins`
|
|
4272
|
+
* call after this returns.
|
|
4273
|
+
*
|
|
4274
|
+
* Refuses to uninstall plugins whose `source` is not `"registry"` to
|
|
4275
|
+
* avoid trashing a marketplace/config plugin that happens to share the
|
|
4276
|
+
* pluginId namespace.
|
|
4277
|
+
*/
|
|
4278
|
+
async function handleRegistryUninstall(db, storage, pluginId, opts) {
|
|
4279
|
+
try {
|
|
4280
|
+
const stateRepo = new PluginStateRepository(db);
|
|
4281
|
+
const existing = await stateRepo.get(pluginId);
|
|
4282
|
+
if (!existing || existing.source !== "registry") return {
|
|
4283
|
+
success: false,
|
|
4284
|
+
error: {
|
|
4285
|
+
code: "NOT_FOUND",
|
|
4286
|
+
message: `No registry plugin found: ${pluginId}`
|
|
4287
|
+
}
|
|
4288
|
+
};
|
|
4289
|
+
const version = existing.version;
|
|
4290
|
+
await opts?.beforeDelete?.();
|
|
4291
|
+
let dataDeleted = false;
|
|
4292
|
+
if (opts?.deleteData) {
|
|
4293
|
+
await db.deleteFrom("_plugin_storage").where("plugin_id", "=", pluginId).execute();
|
|
4294
|
+
dataDeleted = true;
|
|
4295
|
+
}
|
|
4296
|
+
try {
|
|
4297
|
+
await removeAllPluginIndexes(db, pluginId);
|
|
4298
|
+
} catch {}
|
|
4299
|
+
await stateRepo.delete(pluginId);
|
|
4300
|
+
if (storage) await deleteBundleFromR2(storage, pluginId, version, "registry");
|
|
4301
|
+
return {
|
|
4302
|
+
success: true,
|
|
4303
|
+
data: {
|
|
4304
|
+
pluginId,
|
|
4305
|
+
dataDeleted
|
|
4306
|
+
}
|
|
4307
|
+
};
|
|
4308
|
+
} catch (err) {
|
|
4309
|
+
console.error("[registry-uninstall] Failed:", err);
|
|
4310
|
+
return {
|
|
4311
|
+
success: false,
|
|
4312
|
+
error: {
|
|
4313
|
+
code: "UNINSTALL_FAILED",
|
|
4314
|
+
message: "Failed to uninstall plugin"
|
|
4315
|
+
}
|
|
4316
|
+
};
|
|
4317
|
+
}
|
|
4318
|
+
}
|
|
4319
|
+
/**
|
|
4320
|
+
* Update a registry-source plugin to a newer release. Mirrors
|
|
4321
|
+
* `handleMarketplaceUpdate`: resolves the target version via the aggregator,
|
|
4322
|
+
* re-runs the artifact fetch / checksum / extract pipeline, diffs capabilities
|
|
4323
|
+
* and route visibility against the currently installed bundle, and gates
|
|
4324
|
+
* escalations behind `confirmCapabilityChanges` and an exact
|
|
4325
|
+
* `acknowledgedPublicRoutes` match so the admin re-consents to widened permissions.
|
|
4326
|
+
*
|
|
4327
|
+
* Refuses non-registry sources. Refuses when the stored state row is missing
|
|
4328
|
+
* the `(publisherDid, slug)` it needs to resolve against the aggregator.
|
|
4329
|
+
*/
|
|
4330
|
+
async function handleRegistryUpdate(db, storage, sandboxRunner, registryConfigInput, pluginId, opts) {
|
|
4331
|
+
const registryConfig = coerceRegistryConfig(registryConfigInput);
|
|
4332
|
+
if (!registryConfig) return {
|
|
4333
|
+
success: false,
|
|
4334
|
+
error: {
|
|
4335
|
+
code: "REGISTRY_NOT_CONFIGURED",
|
|
4336
|
+
message: "Registry is not configured"
|
|
4337
|
+
}
|
|
4338
|
+
};
|
|
4339
|
+
if (!storage) return {
|
|
4340
|
+
success: false,
|
|
4341
|
+
error: {
|
|
4342
|
+
code: "STORAGE_NOT_CONFIGURED",
|
|
4343
|
+
message: "Storage is required for registry plugin updates"
|
|
4344
|
+
}
|
|
4345
|
+
};
|
|
4346
|
+
if (!sandboxRunner || !sandboxRunner.isAvailable()) return {
|
|
4347
|
+
success: false,
|
|
4348
|
+
error: {
|
|
4349
|
+
code: "SANDBOX_NOT_AVAILABLE",
|
|
4350
|
+
message: withUnavailableReason("Sandbox runner is required", sandboxRunner)
|
|
4351
|
+
}
|
|
4352
|
+
};
|
|
4353
|
+
try {
|
|
4354
|
+
validateAggregatorUrl(registryConfig.aggregatorUrl);
|
|
4355
|
+
} catch (err) {
|
|
4356
|
+
return {
|
|
4357
|
+
success: false,
|
|
4358
|
+
error: {
|
|
4359
|
+
code: "REGISTRY_NOT_CONFIGURED",
|
|
4360
|
+
message: err instanceof Error ? err.message : "Invalid aggregator URL"
|
|
4361
|
+
}
|
|
4362
|
+
};
|
|
4363
|
+
}
|
|
4364
|
+
try {
|
|
4365
|
+
const stateRepo = new PluginStateRepository(db);
|
|
4366
|
+
const existing = await stateRepo.get(pluginId);
|
|
4367
|
+
if (!existing || existing.source !== "registry") return {
|
|
4368
|
+
success: false,
|
|
4369
|
+
error: {
|
|
4370
|
+
code: "NOT_FOUND",
|
|
4371
|
+
message: `No registry plugin found: ${pluginId}`
|
|
4372
|
+
}
|
|
4373
|
+
};
|
|
4374
|
+
if (!existing.registryPublisherDid || !existing.registrySlug) return {
|
|
4375
|
+
success: false,
|
|
4376
|
+
error: {
|
|
4377
|
+
code: "INVALID_STATE",
|
|
4378
|
+
message: `Registry plugin ${pluginId} is missing publisher DID or slug in state`
|
|
4379
|
+
}
|
|
4380
|
+
};
|
|
4381
|
+
const oldVersion = existing.version;
|
|
4382
|
+
const publisherDid = existing.registryPublisherDid;
|
|
4383
|
+
const slug = existing.registrySlug;
|
|
4384
|
+
const { DiscoveryClient, registryLabelerPolicy } = await import("@emdash-cms/registry-client/discovery");
|
|
4385
|
+
const aggregatorDeadline = Date.now() + AGGREGATOR_TOTAL_BUDGET_MS;
|
|
4386
|
+
const discovery = new DiscoveryClient({
|
|
4387
|
+
aggregatorUrl: registryConfig.aggregatorUrl,
|
|
4388
|
+
acceptLabelers: registryConfig.acceptLabelers,
|
|
4389
|
+
labelerPolicy: registryLabelerPolicy(registryConfig.acceptLabelers),
|
|
4390
|
+
fetch: timedFetch(aggregatorDeadline)
|
|
4391
|
+
});
|
|
4392
|
+
const MAX_LIST_PAGES = 20;
|
|
4393
|
+
const releaseView = await (async () => {
|
|
4394
|
+
if (!opts?.version) return discovery.getLatestRelease({
|
|
4395
|
+
did: publisherDid,
|
|
4396
|
+
package: slug
|
|
4397
|
+
});
|
|
4398
|
+
let cursor;
|
|
4399
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
4400
|
+
for (let page = 0; page < MAX_LIST_PAGES; page++) {
|
|
4401
|
+
if (cursor !== void 0) {
|
|
4402
|
+
if (seenCursors.has(cursor)) break;
|
|
4403
|
+
seenCursors.add(cursor);
|
|
4404
|
+
}
|
|
4405
|
+
const result = await discovery.listReleases({
|
|
4406
|
+
did: publisherDid,
|
|
4407
|
+
package: slug,
|
|
4408
|
+
cursor,
|
|
4409
|
+
limit: 50
|
|
4410
|
+
});
|
|
4411
|
+
for (const r of result.releases) if (r.version === opts.version) return r;
|
|
4412
|
+
if (!result.cursor) break;
|
|
4413
|
+
cursor = result.cursor;
|
|
4414
|
+
}
|
|
4415
|
+
})();
|
|
4416
|
+
if (!releaseView) return {
|
|
4417
|
+
success: false,
|
|
4418
|
+
error: {
|
|
4419
|
+
code: "NO_VERSION",
|
|
4420
|
+
message: opts?.version ? `Version ${opts.version} not found for ${publisherDid}/${slug}` : `No installable release found for ${publisherDid}/${slug}`
|
|
4421
|
+
}
|
|
4422
|
+
};
|
|
4423
|
+
if (releaseView.did !== publisherDid || releaseView.package !== slug || opts?.version !== void 0 && releaseView.version !== opts.version) return {
|
|
4424
|
+
success: false,
|
|
4425
|
+
error: {
|
|
4426
|
+
code: "AGGREGATOR_IDENTITY_MISMATCH",
|
|
4427
|
+
message: "Aggregator returned a release view that does not match the requested package or version."
|
|
4428
|
+
}
|
|
4429
|
+
};
|
|
4430
|
+
const packageView = await discovery.getPackage({
|
|
4431
|
+
did: publisherDid,
|
|
4432
|
+
slug
|
|
4433
|
+
});
|
|
4434
|
+
if (!packageView || packageView.did !== publisherDid || packageView.slug !== slug) return {
|
|
4435
|
+
success: false,
|
|
4436
|
+
error: {
|
|
4437
|
+
code: "AGGREGATOR_IDENTITY_MISMATCH",
|
|
4438
|
+
message: "Aggregator returned a package view that does not match the installed plugin."
|
|
4439
|
+
}
|
|
4440
|
+
};
|
|
4441
|
+
const newVersion = releaseView.version;
|
|
4442
|
+
if (evaluateRegistryReleaseWithdrawal(releaseView, discovery.labelerPolicy).withdrawn) return {
|
|
4443
|
+
success: false,
|
|
4444
|
+
error: {
|
|
4445
|
+
code: "YANKED",
|
|
4446
|
+
message: "Release has been withdrawn"
|
|
4447
|
+
}
|
|
4448
|
+
};
|
|
4449
|
+
if (newVersion === oldVersion) return {
|
|
4450
|
+
success: false,
|
|
4451
|
+
error: {
|
|
4452
|
+
code: "ALREADY_UP_TO_DATE",
|
|
4453
|
+
message: "Plugin is already at the requested version"
|
|
4454
|
+
}
|
|
4455
|
+
};
|
|
4456
|
+
const authoritative = await (opts?.readAuthoritativeRecords ?? readAuthoritativePackageRelease)(publisherDid, slug, newVersion, opts?.authoritativeRecords);
|
|
4457
|
+
if (!authoritative.success) return registryRecordError(authoritative.error.code, authoritative.error.message);
|
|
4458
|
+
const records = authoritative.value;
|
|
4459
|
+
const { profile, release } = records.inspection.value;
|
|
4460
|
+
if (packageView.uri !== records.profile.uri || packageView.cid !== records.profile.cid || releaseView.uri !== records.release.uri || releaseView.cid !== records.release.cid) return {
|
|
4461
|
+
success: false,
|
|
4462
|
+
error: {
|
|
4463
|
+
code: "AGGREGATOR_RECORD_MISMATCH",
|
|
4464
|
+
message: "Aggregator metadata does not match the publisher's signed records."
|
|
4465
|
+
}
|
|
4466
|
+
};
|
|
4467
|
+
if (hasCurrentRecordLabel(packageView.labels ?? [], "security:yanked", records.profile) || hasCurrentRecordLabel(packageView.labels ?? [], "security-yanked", records.profile)) return {
|
|
4468
|
+
success: false,
|
|
4469
|
+
error: {
|
|
4470
|
+
code: "YANKED",
|
|
4471
|
+
message: "Package has been withdrawn"
|
|
4472
|
+
}
|
|
4473
|
+
};
|
|
4474
|
+
if (opts?.confirmCapabilityChanges || (opts?.acknowledgedPublicRoutes?.length ?? 0) > 0 || opts?.confirmMcpTools) {
|
|
4475
|
+
const consentError = recordConsentError({
|
|
4476
|
+
profileCid: opts?.acknowledgedProfileCid,
|
|
4477
|
+
releaseCid: opts?.acknowledgedReleaseCid
|
|
4478
|
+
}, records);
|
|
4479
|
+
if (consentError) return consentError;
|
|
4480
|
+
}
|
|
4481
|
+
if (opts?.hostEnv) {
|
|
4482
|
+
const envError = assertEnvCompatible(release.requires, opts.hostEnv);
|
|
4483
|
+
if (envError) return {
|
|
4484
|
+
success: false,
|
|
4485
|
+
error: envError
|
|
4486
|
+
};
|
|
4487
|
+
}
|
|
4488
|
+
const packageArtifact = release.artifacts.package;
|
|
4489
|
+
const declaredChecksum = packageArtifact?.checksum;
|
|
4490
|
+
if (!packageArtifact || !declaredChecksum) return {
|
|
4491
|
+
success: false,
|
|
4492
|
+
error: {
|
|
4493
|
+
code: "INVALID_RELEASE",
|
|
4494
|
+
message: "Release record is missing its package artifact or checksum"
|
|
4495
|
+
}
|
|
4496
|
+
};
|
|
4497
|
+
let artifactBytes;
|
|
4498
|
+
try {
|
|
4499
|
+
artifactBytes = await fetchArtifact(releaseView.artifactCaches ?? [], packageArtifact, {
|
|
4500
|
+
did: publisherDid,
|
|
4501
|
+
collection: NSID.packageRelease,
|
|
4502
|
+
rkey: `${slug}:${newVersion}`,
|
|
4503
|
+
cid: releaseView.cid
|
|
4504
|
+
}, release.auth);
|
|
4505
|
+
} catch (error) {
|
|
4506
|
+
if (error instanceof RegistryArtifactFetchError) return registryArtifactError(error.code, error.message, "update");
|
|
4507
|
+
throw error;
|
|
4508
|
+
}
|
|
4509
|
+
const artifactReport = await validateRegistryArtifact(artifactBytes, declaredChecksum, slug, newVersion);
|
|
4510
|
+
if (!artifactReport.success) return registryArtifactError(artifactReport.error.code, artifactReport.error.message, "update");
|
|
4511
|
+
const { bundle, artifactDigest, artifactDigests } = artifactReport.value;
|
|
4512
|
+
const recordReport = await verifyAuthoritativePackageRelease(records, artifactDigest, {
|
|
4513
|
+
...opts?.authoritativeRecords,
|
|
4514
|
+
artifactDigests
|
|
4515
|
+
});
|
|
4516
|
+
if (!recordReport.success) return registryRecordError(recordReport.code, recordReport.reasons[0]?.message ?? "The release provenance is invalid.");
|
|
4517
|
+
const verification = recordVerificationSummary(records, recordReport);
|
|
4518
|
+
bundle.manifest = {
|
|
4519
|
+
...bundle.manifest,
|
|
4520
|
+
id: pluginId
|
|
4521
|
+
};
|
|
4522
|
+
if (!verifiedAccessEqual(recordReport.value.declaredAccess, bundle.manifest.declaredAccess ?? {})) return {
|
|
4523
|
+
success: false,
|
|
4524
|
+
error: {
|
|
4525
|
+
code: "DECLARED_ACCESS_DRIFT",
|
|
4526
|
+
message: "The plugin bundle declares different permissions than its published record. Update refused."
|
|
4527
|
+
}
|
|
4528
|
+
};
|
|
4529
|
+
const oldBundle = await loadBundleFromR2(storage, pluginId, oldVersion, "registry");
|
|
4530
|
+
const capabilityChanges = diffCapabilities(oldBundle?.manifest.capabilities ?? [], bundle.manifest.capabilities);
|
|
4531
|
+
if (capabilityChanges.added.length > 0 && !opts?.confirmCapabilityChanges) return {
|
|
4532
|
+
success: false,
|
|
4533
|
+
error: {
|
|
4534
|
+
code: "CAPABILITY_ESCALATION",
|
|
4535
|
+
message: "Plugin update requires new capabilities",
|
|
4536
|
+
details: {
|
|
4537
|
+
capabilityChanges,
|
|
4538
|
+
verification
|
|
4539
|
+
}
|
|
4540
|
+
}
|
|
4541
|
+
};
|
|
4542
|
+
const newlyPublicRoutes = diffRouteVisibility(oldBundle?.manifest, bundle.manifest).newlyPublic.toSorted();
|
|
4543
|
+
const acknowledgedPublicRoutes = (opts?.acknowledgedPublicRoutes ?? []).filter((route) => typeof route === "string").toSorted();
|
|
4544
|
+
if (newlyPublicRoutes.length > 0 && JSON.stringify(acknowledgedPublicRoutes) !== JSON.stringify(newlyPublicRoutes)) return {
|
|
4545
|
+
success: false,
|
|
4546
|
+
error: {
|
|
4547
|
+
code: "ROUTE_VISIBILITY_ESCALATION",
|
|
4548
|
+
message: "Plugin update exposes new public (unauthenticated) routes",
|
|
4549
|
+
details: {
|
|
4550
|
+
routeVisibilityChanges: { newlyPublic: newlyPublicRoutes },
|
|
4551
|
+
capabilityChanges,
|
|
4552
|
+
verification
|
|
4553
|
+
}
|
|
4554
|
+
}
|
|
4555
|
+
};
|
|
4556
|
+
const oldMcpTools = [...oldBundle?.manifest.mcp?.tools ?? []].toSorted((a, b) => a.name.localeCompare(b.name));
|
|
4557
|
+
const newMcpTools = [...bundle.manifest.mcp?.tools ?? []].toSorted((a, b) => a.name.localeCompare(b.name));
|
|
4558
|
+
if (JSON.stringify(oldMcpTools) !== JSON.stringify(newMcpTools) && !opts?.confirmMcpTools) return {
|
|
4559
|
+
success: false,
|
|
4560
|
+
error: {
|
|
4561
|
+
code: "MCP_TOOL_CONSENT_REQUIRED",
|
|
4562
|
+
message: "Plugin update changes its MCP tools",
|
|
4563
|
+
details: {
|
|
4564
|
+
mcpTools: newMcpTools.map(({ inputSchema: _, outputSchema: __, ...tool }) => tool),
|
|
4565
|
+
verification
|
|
4566
|
+
}
|
|
4567
|
+
}
|
|
4568
|
+
};
|
|
4569
|
+
await storeBundleInR2(storage, pluginId, newVersion, bundle, "registry");
|
|
4570
|
+
await stateRepo.upsert(pluginId, newVersion, "active", {
|
|
4571
|
+
source: "registry",
|
|
4572
|
+
registryPublisherDid: publisherDid,
|
|
4573
|
+
registrySlug: slug,
|
|
4574
|
+
displayName: profile.name ?? slug,
|
|
4575
|
+
description: profile.description ?? void 0,
|
|
4576
|
+
mcpToolsEnabled: false,
|
|
4577
|
+
mcpToolsConsent: null
|
|
4578
|
+
});
|
|
4579
|
+
await syncDeclaredStorageIndexes(db, [bundle.manifest]);
|
|
4580
|
+
return {
|
|
4581
|
+
success: true,
|
|
4582
|
+
data: {
|
|
4583
|
+
pluginId,
|
|
4584
|
+
oldVersion,
|
|
4585
|
+
newVersion,
|
|
4586
|
+
capabilityChanges,
|
|
4587
|
+
routeVisibilityChanges: newlyPublicRoutes.length > 0 ? { newlyPublic: newlyPublicRoutes } : void 0,
|
|
4588
|
+
verification
|
|
4589
|
+
}
|
|
4590
|
+
};
|
|
4591
|
+
} catch (err) {
|
|
4592
|
+
if (err instanceof ClientValidationError) return {
|
|
4593
|
+
success: false,
|
|
4594
|
+
error: {
|
|
4595
|
+
code: "AGGREGATOR_RESPONSE_INVALID",
|
|
4596
|
+
message: `Aggregator returned a response that does not conform to its lexicon (${err.target})`
|
|
4597
|
+
}
|
|
4598
|
+
};
|
|
4599
|
+
if (err instanceof ClientResponseError) {
|
|
4600
|
+
if (err.error === "ListingUnavailable") return {
|
|
4601
|
+
success: false,
|
|
4602
|
+
error: {
|
|
4603
|
+
code: "LISTING_UNAVAILABLE",
|
|
4604
|
+
message: "This plugin is unavailable under the active registry policy"
|
|
4605
|
+
}
|
|
4606
|
+
};
|
|
4607
|
+
return {
|
|
4608
|
+
success: false,
|
|
4609
|
+
error: {
|
|
4610
|
+
code: err.status === 404 ? "AGGREGATOR_NOT_FOUND" : "AGGREGATOR_HTTP_ERROR",
|
|
4611
|
+
message: `Aggregator returned ${err.status}: ${err.error}`
|
|
4612
|
+
}
|
|
4613
|
+
};
|
|
4614
|
+
}
|
|
4615
|
+
if (err instanceof EmDashStorageError) return {
|
|
4616
|
+
success: false,
|
|
4617
|
+
error: {
|
|
4618
|
+
code: err.code ?? "STORAGE_ERROR",
|
|
4619
|
+
message: "Storage error while updating plugin"
|
|
4620
|
+
}
|
|
4621
|
+
};
|
|
4622
|
+
console.error("[registry-update] Failed:", err);
|
|
4623
|
+
return {
|
|
4624
|
+
success: false,
|
|
4625
|
+
error: {
|
|
4626
|
+
code: "UPDATE_FAILED",
|
|
4627
|
+
message: err instanceof Error ? err.message : "Failed to update plugin"
|
|
4628
|
+
}
|
|
4629
|
+
};
|
|
4630
|
+
}
|
|
4631
|
+
}
|
|
4632
|
+
/**
|
|
4633
|
+
* Bulk update check across every installed registry plugin. Queries the
|
|
4634
|
+
* aggregator for each plugin's latest release and reports `hasUpdate`
|
|
4635
|
+
* based on the version comparison. Plugins whose aggregator lookup fails
|
|
4636
|
+
* (unreachable, delisted, malformed) are skipped silently — one bad
|
|
4637
|
+
* publisher must not blank the whole admin Updates list.
|
|
4638
|
+
*/
|
|
4639
|
+
async function handleRegistryUpdateCheck(db, registryConfigInput) {
|
|
4640
|
+
const registryConfig = coerceRegistryConfig(registryConfigInput);
|
|
4641
|
+
if (!registryConfig) return {
|
|
4642
|
+
success: false,
|
|
4643
|
+
error: {
|
|
4644
|
+
code: "REGISTRY_NOT_CONFIGURED",
|
|
4645
|
+
message: "Registry is not configured"
|
|
4646
|
+
}
|
|
4647
|
+
};
|
|
4648
|
+
try {
|
|
4649
|
+
const registryPlugins = await new PluginStateRepository(db).getRegistryPlugins();
|
|
4650
|
+
if (registryPlugins.length === 0) return {
|
|
4651
|
+
success: true,
|
|
4652
|
+
data: { items: [] }
|
|
4653
|
+
};
|
|
4654
|
+
const { DiscoveryClient, registryLabelerPolicy } = await import("@emdash-cms/registry-client/discovery");
|
|
4655
|
+
const aggregatorDeadline = Date.now() + AGGREGATOR_TOTAL_BUDGET_MS;
|
|
4656
|
+
const discovery = new DiscoveryClient({
|
|
4657
|
+
aggregatorUrl: registryConfig.aggregatorUrl,
|
|
4658
|
+
acceptLabelers: registryConfig.acceptLabelers,
|
|
4659
|
+
labelerPolicy: registryLabelerPolicy(registryConfig.acceptLabelers),
|
|
4660
|
+
fetch: timedFetch(aggregatorDeadline)
|
|
4661
|
+
});
|
|
4662
|
+
const items = [];
|
|
4663
|
+
for (const plugin of registryPlugins) {
|
|
4664
|
+
if (!plugin.registryPublisherDid || !plugin.registrySlug) continue;
|
|
4665
|
+
try {
|
|
4666
|
+
const releaseView = await discovery.getLatestRelease({
|
|
4667
|
+
did: plugin.registryPublisherDid,
|
|
4668
|
+
package: plugin.registrySlug
|
|
4669
|
+
});
|
|
4670
|
+
if (evaluateRegistryReleaseWithdrawal(releaseView, discovery.labelerPolicy).withdrawn) continue;
|
|
4671
|
+
const latest = releaseView.version;
|
|
4672
|
+
if (!latest) continue;
|
|
4673
|
+
const installed = plugin.version;
|
|
4674
|
+
items.push({
|
|
4675
|
+
pluginId: plugin.pluginId,
|
|
4676
|
+
installed,
|
|
4677
|
+
latest,
|
|
4678
|
+
hasUpdate: latest !== installed,
|
|
4679
|
+
hasCapabilityChanges: false,
|
|
4680
|
+
hasRouteVisibilityChanges: false
|
|
4681
|
+
});
|
|
4682
|
+
} catch (err) {
|
|
4683
|
+
console.warn(`[registry-update-check] Skipped ${plugin.pluginId}:`, err);
|
|
4684
|
+
}
|
|
4685
|
+
}
|
|
4686
|
+
return {
|
|
4687
|
+
success: true,
|
|
4688
|
+
data: { items }
|
|
4689
|
+
};
|
|
4690
|
+
} catch (err) {
|
|
4691
|
+
if (err instanceof ClientValidationError) return {
|
|
4692
|
+
success: false,
|
|
4693
|
+
error: {
|
|
4694
|
+
code: "AGGREGATOR_RESPONSE_INVALID",
|
|
4695
|
+
message: `Aggregator returned a response that does not conform to its lexicon (${err.target})`
|
|
4696
|
+
}
|
|
4697
|
+
};
|
|
4698
|
+
if (err instanceof ClientResponseError) return {
|
|
4699
|
+
success: false,
|
|
4700
|
+
error: {
|
|
4701
|
+
code: err.status === 404 ? "AGGREGATOR_NOT_FOUND" : "AGGREGATOR_HTTP_ERROR",
|
|
4702
|
+
message: `Aggregator returned ${err.status}: ${err.error}`
|
|
4703
|
+
}
|
|
4704
|
+
};
|
|
4705
|
+
console.error("[registry-update-check] Failed:", err);
|
|
4706
|
+
return {
|
|
4707
|
+
success: false,
|
|
4708
|
+
error: {
|
|
4709
|
+
code: "UPDATE_CHECK_FAILED",
|
|
4710
|
+
message: "Failed to check for registry updates"
|
|
4711
|
+
}
|
|
4712
|
+
};
|
|
4713
|
+
}
|
|
4714
|
+
}
|
|
4715
|
+
|
|
4716
|
+
//#endregion
|
|
4717
|
+
export { handleContentDiscardDraft as A, handleContentTranslations as B, generateManifest as C, handleContentCountTrashed as D, handleContentCountScheduled as E, handleContentListTrashed as F, validateRev as G, handleContentUnschedule as H, handleContentPermanentDelete as I, handleContentPublish as L, handleContentGet as M, handleContentGetIncludingTrashed as N, handleContentCreate as O, handleContentList as P, handleContentRestore as R, buildManifestCollections as S, handleContentCompare as T, handleContentUpdate as U, handleContentUnpublish as V, handleScheduledPolicyRejection as W, handlePluginSettingsGet as _, handleMarketplaceGetPlugin as a, handleRevisionList as b, handleMarketplaceUninstall as c, handleThemeGetDetail as d, handleThemeSearch as f, getPluginSettingsSchema as g, syncDeclaredStorageIndexes as h, handleRegistryUpdateCheck as i, handleContentDuplicate as j, handleContentDelete as k, handleMarketplaceUpdate as l, rollbackPluginUpdate as m, handleRegistryUninstall as n, handleMarketplaceInstall as o, loadBundleFromR2 as p, handleRegistryUpdate as r, handleMarketplaceSearch as s, handleRegistryInstall as t, handleMarketplaceUpdateCheck as u, handlePluginSettingsUpdate as v, handleContentAuthors as w, handleRevisionRestore as x, handleRevisionGet as y, handleContentSchedule as z };
|
|
4718
|
+
//# sourceMappingURL=api-cFFZpyS0.mjs.map
|