@growth-labs/cms 0.1.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/README.md +165 -0
- package/dist/engine/activity-log.d.ts +17 -0
- package/dist/engine/activity-log.d.ts.map +1 -0
- package/dist/engine/activity-log.js +17 -0
- package/dist/engine/activity-log.js.map +1 -0
- package/dist/engine/ai-prompts.d.ts +57 -0
- package/dist/engine/ai-prompts.d.ts.map +1 -0
- package/dist/engine/ai-prompts.js +90 -0
- package/dist/engine/ai-prompts.js.map +1 -0
- package/dist/engine/ai-writeback.d.ts +36 -0
- package/dist/engine/ai-writeback.d.ts.map +1 -0
- package/dist/engine/ai-writeback.js +45 -0
- package/dist/engine/ai-writeback.js.map +1 -0
- package/dist/engine/api-keys.d.ts +76 -0
- package/dist/engine/api-keys.d.ts.map +1 -0
- package/dist/engine/api-keys.js +165 -0
- package/dist/engine/api-keys.js.map +1 -0
- package/dist/engine/content-insights.d.ts +36 -0
- package/dist/engine/content-insights.d.ts.map +1 -0
- package/dist/engine/content-insights.js +114 -0
- package/dist/engine/content-insights.js.map +1 -0
- package/dist/engine/contributors.d.ts +25 -0
- package/dist/engine/contributors.d.ts.map +1 -0
- package/dist/engine/contributors.js +59 -0
- package/dist/engine/contributors.js.map +1 -0
- package/dist/engine/cron.d.ts +15 -0
- package/dist/engine/cron.d.ts.map +1 -0
- package/dist/engine/cron.js +33 -0
- package/dist/engine/cron.js.map +1 -0
- package/dist/engine/d1.d.ts +16 -0
- package/dist/engine/d1.d.ts.map +1 -0
- package/dist/engine/d1.js +13 -0
- package/dist/engine/d1.js.map +1 -0
- package/dist/engine/foundry-dispatch.d.ts +52 -0
- package/dist/engine/foundry-dispatch.d.ts.map +1 -0
- package/dist/engine/foundry-dispatch.js +290 -0
- package/dist/engine/foundry-dispatch.js.map +1 -0
- package/dist/engine/import-parsers.d.ts +11 -0
- package/dist/engine/import-parsers.d.ts.map +1 -0
- package/dist/engine/import-parsers.js +373 -0
- package/dist/engine/import-parsers.js.map +1 -0
- package/dist/engine/index.d.ts +28 -0
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/engine/index.js +57 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/engine/invites.d.ts +78 -0
- package/dist/engine/invites.d.ts.map +1 -0
- package/dist/engine/invites.js +158 -0
- package/dist/engine/invites.js.map +1 -0
- package/dist/engine/members.d.ts +59 -0
- package/dist/engine/members.d.ts.map +1 -0
- package/dist/engine/members.js +124 -0
- package/dist/engine/members.js.map +1 -0
- package/dist/engine/membership-rules.d.ts +25 -0
- package/dist/engine/membership-rules.d.ts.map +1 -0
- package/dist/engine/membership-rules.js +44 -0
- package/dist/engine/membership-rules.js.map +1 -0
- package/dist/engine/og-render.d.ts +40 -0
- package/dist/engine/og-render.d.ts.map +1 -0
- package/dist/engine/og-render.js +26 -0
- package/dist/engine/og-render.js.map +1 -0
- package/dist/engine/publish-guard.d.ts +58 -0
- package/dist/engine/publish-guard.d.ts.map +1 -0
- package/dist/engine/publish-guard.js +80 -0
- package/dist/engine/publish-guard.js.map +1 -0
- package/dist/engine/publisher.d.ts +171 -0
- package/dist/engine/publisher.d.ts.map +1 -0
- package/dist/engine/publisher.js +597 -0
- package/dist/engine/publisher.js.map +1 -0
- package/dist/engine/revisions.d.ts +39 -0
- package/dist/engine/revisions.d.ts.map +1 -0
- package/dist/engine/revisions.js +203 -0
- package/dist/engine/revisions.js.map +1 -0
- package/dist/engine/sanitize.d.ts +52 -0
- package/dist/engine/sanitize.d.ts.map +1 -0
- package/dist/engine/sanitize.js +155 -0
- package/dist/engine/sanitize.js.map +1 -0
- package/dist/engine/seed-membership.d.ts +29 -0
- package/dist/engine/seed-membership.d.ts.map +1 -0
- package/dist/engine/seed-membership.js +65 -0
- package/dist/engine/seed-membership.js.map +1 -0
- package/dist/engine/seo.d.ts +20 -0
- package/dist/engine/seo.d.ts.map +1 -0
- package/dist/engine/seo.js +50 -0
- package/dist/engine/seo.js.map +1 -0
- package/dist/engine/slug-redirects.d.ts +8 -0
- package/dist/engine/slug-redirects.d.ts.map +1 -0
- package/dist/engine/slug-redirects.js +26 -0
- package/dist/engine/slug-redirects.js.map +1 -0
- package/dist/engine/slug.d.ts +6 -0
- package/dist/engine/slug.d.ts.map +1 -0
- package/dist/engine/slug.js +28 -0
- package/dist/engine/slug.js.map +1 -0
- package/dist/engine/soft-delete.d.ts +8 -0
- package/dist/engine/soft-delete.d.ts.map +1 -0
- package/dist/engine/soft-delete.js +28 -0
- package/dist/engine/soft-delete.js.map +1 -0
- package/dist/engine/tags.d.ts +14 -0
- package/dist/engine/tags.d.ts.map +1 -0
- package/dist/engine/tags.js +79 -0
- package/dist/engine/tags.js.map +1 -0
- package/dist/engine/topics.d.ts +10 -0
- package/dist/engine/topics.d.ts.map +1 -0
- package/dist/engine/topics.js +140 -0
- package/dist/engine/topics.js.map +1 -0
- package/dist/engine/url-guard.d.ts +12 -0
- package/dist/engine/url-guard.d.ts.map +1 -0
- package/dist/engine/url-guard.js +129 -0
- package/dist/engine/url-guard.js.map +1 -0
- package/dist/engine/validator/checks/bare-url-not-autolinked.d.ts +20 -0
- package/dist/engine/validator/checks/bare-url-not-autolinked.d.ts.map +1 -0
- package/dist/engine/validator/checks/bare-url-not-autolinked.js +54 -0
- package/dist/engine/validator/checks/bare-url-not-autolinked.js.map +1 -0
- package/dist/engine/validator/checks/broken-footnote-label.d.ts +16 -0
- package/dist/engine/validator/checks/broken-footnote-label.d.ts.map +1 -0
- package/dist/engine/validator/checks/broken-footnote-label.js +17 -0
- package/dist/engine/validator/checks/broken-footnote-label.js.map +1 -0
- package/dist/engine/validator/checks/double-encoded-entities.d.ts +18 -0
- package/dist/engine/validator/checks/double-encoded-entities.d.ts.map +1 -0
- package/dist/engine/validator/checks/double-encoded-entities.js +23 -0
- package/dist/engine/validator/checks/double-encoded-entities.js.map +1 -0
- package/dist/engine/validator/checks/empty-alt-text.d.ts +14 -0
- package/dist/engine/validator/checks/empty-alt-text.d.ts.map +1 -0
- package/dist/engine/validator/checks/empty-alt-text.js +23 -0
- package/dist/engine/validator/checks/empty-alt-text.js.map +1 -0
- package/dist/engine/validator/checks/heading-hierarchy-skip.d.ts +11 -0
- package/dist/engine/validator/checks/heading-hierarchy-skip.d.ts.map +1 -0
- package/dist/engine/validator/checks/heading-hierarchy-skip.js +20 -0
- package/dist/engine/validator/checks/heading-hierarchy-skip.js.map +1 -0
- package/dist/engine/validator/checks/html-comment-leak.d.ts +20 -0
- package/dist/engine/validator/checks/html-comment-leak.d.ts.map +1 -0
- package/dist/engine/validator/checks/html-comment-leak.js +30 -0
- package/dist/engine/validator/checks/html-comment-leak.js.map +1 -0
- package/dist/engine/validator/checks/iframe-missing-dims-and-wrapper.d.ts +12 -0
- package/dist/engine/validator/checks/iframe-missing-dims-and-wrapper.d.ts.map +1 -0
- package/dist/engine/validator/checks/iframe-missing-dims-and-wrapper.js +17 -0
- package/dist/engine/validator/checks/iframe-missing-dims-and-wrapper.js.map +1 -0
- package/dist/engine/validator/checks/invisible-control-chars.d.ts +24 -0
- package/dist/engine/validator/checks/invisible-control-chars.d.ts.map +1 -0
- package/dist/engine/validator/checks/invisible-control-chars.js +30 -0
- package/dist/engine/validator/checks/invisible-control-chars.js.map +1 -0
- package/dist/engine/validator/checks/paywall-marker-leak.d.ts +17 -0
- package/dist/engine/validator/checks/paywall-marker-leak.d.ts.map +1 -0
- package/dist/engine/validator/checks/paywall-marker-leak.js +22 -0
- package/dist/engine/validator/checks/paywall-marker-leak.js.map +1 -0
- package/dist/engine/validator/checks/raw-block-html.d.ts +28 -0
- package/dist/engine/validator/checks/raw-block-html.d.ts.map +1 -0
- package/dist/engine/validator/checks/raw-block-html.js +38 -0
- package/dist/engine/validator/checks/raw-block-html.js.map +1 -0
- package/dist/engine/validator/checks/stale-body-html.d.ts +28 -0
- package/dist/engine/validator/checks/stale-body-html.d.ts.map +1 -0
- package/dist/engine/validator/checks/stale-body-html.js +15 -0
- package/dist/engine/validator/checks/stale-body-html.js.map +1 -0
- package/dist/engine/validator/checks/unresolved-footnote-anchor.d.ts +11 -0
- package/dist/engine/validator/checks/unresolved-footnote-anchor.d.ts.map +1 -0
- package/dist/engine/validator/checks/unresolved-footnote-anchor.js +48 -0
- package/dist/engine/validator/checks/unresolved-footnote-anchor.js.map +1 -0
- package/dist/engine/validator/checks/word-gdocs-paste-artifacts.d.ts +23 -0
- package/dist/engine/validator/checks/word-gdocs-paste-artifacts.d.ts.map +1 -0
- package/dist/engine/validator/checks/word-gdocs-paste-artifacts.js +47 -0
- package/dist/engine/validator/checks/word-gdocs-paste-artifacts.js.map +1 -0
- package/dist/engine/validator/index.d.ts +75 -0
- package/dist/engine/validator/index.d.ts.map +1 -0
- package/dist/engine/validator/index.js +313 -0
- package/dist/engine/validator/index.js.map +1 -0
- package/dist/engine/validator/scan.d.ts +28 -0
- package/dist/engine/validator/scan.d.ts.map +1 -0
- package/dist/engine/validator/scan.js +97 -0
- package/dist/engine/validator/scan.js.map +1 -0
- package/dist/engine/validator/types.d.ts +50 -0
- package/dist/engine/validator/types.d.ts.map +1 -0
- package/dist/engine/validator/types.js +51 -0
- package/dist/engine/validator/types.js.map +1 -0
- package/dist/engine/webhook-signer.d.ts +39 -0
- package/dist/engine/webhook-signer.d.ts.map +1 -0
- package/dist/engine/webhook-signer.js +117 -0
- package/dist/engine/webhook-signer.js.map +1 -0
- package/dist/engine/webhooks.d.ts +75 -0
- package/dist/engine/webhooks.d.ts.map +1 -0
- package/dist/engine/webhooks.js +139 -0
- package/dist/engine/webhooks.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/integration/index.d.ts +6 -0
- package/dist/integration/index.d.ts.map +1 -0
- package/dist/integration/index.js +294 -0
- package/dist/integration/index.js.map +1 -0
- package/dist/integration/options.d.ts +105 -0
- package/dist/integration/options.d.ts.map +1 -0
- package/dist/integration/options.js +25 -0
- package/dist/integration/options.js.map +1 -0
- package/dist/integration/vite-plugin.d.ts +4 -0
- package/dist/integration/vite-plugin.d.ts.map +1 -0
- package/dist/integration/vite-plugin.js +37 -0
- package/dist/integration/vite-plugin.js.map +1 -0
- package/dist/providers/index.d.ts +3 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +3 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/null.d.ts +9 -0
- package/dist/providers/null.d.ts.map +1 -0
- package/dist/providers/null.js +144 -0
- package/dist/providers/null.js.map +1 -0
- package/dist/providers/types.d.ts +277 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +2 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/routes/ai.d.ts +25 -0
- package/dist/routes/ai.d.ts.map +1 -0
- package/dist/routes/ai.js +381 -0
- package/dist/routes/ai.js.map +1 -0
- package/dist/routes/analytics.d.ts +15 -0
- package/dist/routes/analytics.d.ts.map +1 -0
- package/dist/routes/analytics.js +61 -0
- package/dist/routes/analytics.js.map +1 -0
- package/dist/routes/api-keys.d.ts +13 -0
- package/dist/routes/api-keys.d.ts.map +1 -0
- package/dist/routes/api-keys.js +109 -0
- package/dist/routes/api-keys.js.map +1 -0
- package/dist/routes/authors.d.ts +19 -0
- package/dist/routes/authors.d.ts.map +1 -0
- package/dist/routes/authors.js +202 -0
- package/dist/routes/authors.js.map +1 -0
- package/dist/routes/authz-matrix.d.ts +78 -0
- package/dist/routes/authz-matrix.d.ts.map +1 -0
- package/dist/routes/authz-matrix.js +170 -0
- package/dist/routes/authz-matrix.js.map +1 -0
- package/dist/routes/calendar.d.ts +19 -0
- package/dist/routes/calendar.d.ts.map +1 -0
- package/dist/routes/calendar.js +89 -0
- package/dist/routes/calendar.js.map +1 -0
- package/dist/routes/config.d.ts +70 -0
- package/dist/routes/config.d.ts.map +1 -0
- package/dist/routes/config.js +23 -0
- package/dist/routes/config.js.map +1 -0
- package/dist/routes/content-insights.d.ts +18 -0
- package/dist/routes/content-insights.d.ts.map +1 -0
- package/dist/routes/content-insights.js +137 -0
- package/dist/routes/content-insights.js.map +1 -0
- package/dist/routes/content.d.ts +145 -0
- package/dist/routes/content.d.ts.map +1 -0
- package/dist/routes/content.js +1374 -0
- package/dist/routes/content.js.map +1 -0
- package/dist/routes/context.d.ts +104 -0
- package/dist/routes/context.d.ts.map +1 -0
- package/dist/routes/context.js +26 -0
- package/dist/routes/context.js.map +1 -0
- package/dist/routes/cron.d.ts +8 -0
- package/dist/routes/cron.d.ts.map +1 -0
- package/dist/routes/cron.js +20 -0
- package/dist/routes/cron.js.map +1 -0
- package/dist/routes/dashboard.d.ts +12 -0
- package/dist/routes/dashboard.d.ts.map +1 -0
- package/dist/routes/dashboard.js +113 -0
- package/dist/routes/dashboard.js.map +1 -0
- package/dist/routes/imports.d.ts +10 -0
- package/dist/routes/imports.d.ts.map +1 -0
- package/dist/routes/imports.js +149 -0
- package/dist/routes/imports.js.map +1 -0
- package/dist/routes/index.d.ts +75 -0
- package/dist/routes/index.d.ts.map +1 -0
- package/dist/routes/index.js +141 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/media-lib.d.ts +75 -0
- package/dist/routes/media-lib.d.ts.map +1 -0
- package/dist/routes/media-lib.js +305 -0
- package/dist/routes/media-lib.js.map +1 -0
- package/dist/routes/media.d.ts +32 -0
- package/dist/routes/media.d.ts.map +1 -0
- package/dist/routes/media.js +756 -0
- package/dist/routes/media.js.map +1 -0
- package/dist/routes/preview.d.ts +19 -0
- package/dist/routes/preview.d.ts.map +1 -0
- package/dist/routes/preview.js +150 -0
- package/dist/routes/preview.js.map +1 -0
- package/dist/routes/rbac-invites.d.ts +31 -0
- package/dist/routes/rbac-invites.d.ts.map +1 -0
- package/dist/routes/rbac-invites.js +174 -0
- package/dist/routes/rbac-invites.js.map +1 -0
- package/dist/routes/rbac.d.ts +12 -0
- package/dist/routes/rbac.d.ts.map +1 -0
- package/dist/routes/rbac.js +126 -0
- package/dist/routes/rbac.js.map +1 -0
- package/dist/routes/shell.d.ts +22 -0
- package/dist/routes/shell.d.ts.map +1 -0
- package/dist/routes/shell.js +123 -0
- package/dist/routes/shell.js.map +1 -0
- package/dist/routes/subscriptions.d.ts +21 -0
- package/dist/routes/subscriptions.d.ts.map +1 -0
- package/dist/routes/subscriptions.js +127 -0
- package/dist/routes/subscriptions.js.map +1 -0
- package/dist/routes/tags.d.ts +23 -0
- package/dist/routes/tags.d.ts.map +1 -0
- package/dist/routes/tags.js +68 -0
- package/dist/routes/tags.js.map +1 -0
- package/dist/routes/topics.d.ts +12 -0
- package/dist/routes/topics.d.ts.map +1 -0
- package/dist/routes/topics.js +49 -0
- package/dist/routes/topics.js.map +1 -0
- package/dist/routes/webhooks.d.ts +31 -0
- package/dist/routes/webhooks.d.ts.map +1 -0
- package/dist/routes/webhooks.js +173 -0
- package/dist/routes/webhooks.js.map +1 -0
- package/dist/schema/index.d.ts +4 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +6 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/insights-ingest.d.ts +959 -0
- package/dist/schema/insights-ingest.d.ts.map +1 -0
- package/dist/schema/insights-ingest.js +112 -0
- package/dist/schema/insights-ingest.js.map +1 -0
- package/dist/schema/migrations.d.ts +63 -0
- package/dist/schema/migrations.d.ts.map +1 -0
- package/dist/schema/migrations.js +589 -0
- package/dist/schema/migrations.js.map +1 -0
- package/dist/schema/tables.d.ts +11 -0
- package/dist/schema/tables.d.ts.map +1 -0
- package/dist/schema/tables.js +56 -0
- package/dist/schema/tables.js.map +1 -0
- package/dist/schema/types.d.ts +476 -0
- package/dist/schema/types.d.ts.map +1 -0
- package/dist/schema/types.js +37 -0
- package/dist/schema/types.js.map +1 -0
- package/dist/ui/api/_authz.d.ts +6 -0
- package/dist/ui/api/_authz.d.ts.map +1 -0
- package/dist/ui/api/_authz.js +74 -0
- package/dist/ui/api/_authz.js.map +1 -0
- package/dist/ui/api/_content-config.d.ts +22 -0
- package/dist/ui/api/_content-config.d.ts.map +1 -0
- package/dist/ui/api/_content-config.js +50 -0
- package/dist/ui/api/_content-config.js.map +1 -0
- package/dist/ui/api/activity.d.ts +3 -0
- package/dist/ui/api/activity.d.ts.map +1 -0
- package/dist/ui/api/activity.js +28 -0
- package/dist/ui/api/activity.js.map +1 -0
- package/dist/ui/api/analytics.d.ts +3 -0
- package/dist/ui/api/analytics.d.ts.map +1 -0
- package/dist/ui/api/analytics.js +36 -0
- package/dist/ui/api/analytics.js.map +1 -0
- package/dist/ui/api/authors/[id].d.ts +4 -0
- package/dist/ui/api/authors/[id].d.ts.map +1 -0
- package/dist/ui/api/authors/[id].js +17 -0
- package/dist/ui/api/authors/[id].js.map +1 -0
- package/dist/ui/api/authors.d.ts +4 -0
- package/dist/ui/api/authors.d.ts.map +1 -0
- package/dist/ui/api/authors.js +12 -0
- package/dist/ui/api/authors.js.map +1 -0
- package/dist/ui/api/calendar.d.ts +3 -0
- package/dist/ui/api/calendar.d.ts.map +1 -0
- package/dist/ui/api/calendar.js +16 -0
- package/dist/ui/api/calendar.js.map +1 -0
- package/dist/ui/api/content/[id]/ai/headlines.d.ts +3 -0
- package/dist/ui/api/content/[id]/ai/headlines.d.ts.map +1 -0
- package/dist/ui/api/content/[id]/ai/headlines.js +7 -0
- package/dist/ui/api/content/[id]/ai/headlines.js.map +1 -0
- package/dist/ui/api/content/[id]/ai/meta-description.d.ts +3 -0
- package/dist/ui/api/content/[id]/ai/meta-description.d.ts.map +1 -0
- package/dist/ui/api/content/[id]/ai/meta-description.js +6 -0
- package/dist/ui/api/content/[id]/ai/meta-description.js.map +1 -0
- package/dist/ui/api/content/[id]/ai/og-image.d.ts +3 -0
- package/dist/ui/api/content/[id]/ai/og-image.d.ts.map +1 -0
- package/dist/ui/api/content/[id]/ai/og-image.js +7 -0
- package/dist/ui/api/content/[id]/ai/og-image.js.map +1 -0
- package/dist/ui/api/content/[id]/ai/proofread.d.ts +3 -0
- package/dist/ui/api/content/[id]/ai/proofread.d.ts.map +1 -0
- package/dist/ui/api/content/[id]/ai/proofread.js +7 -0
- package/dist/ui/api/content/[id]/ai/proofread.js.map +1 -0
- package/dist/ui/api/content/[id]/ai/takeaways.d.ts +3 -0
- package/dist/ui/api/content/[id]/ai/takeaways.d.ts.map +1 -0
- package/dist/ui/api/content/[id]/ai/takeaways.js +6 -0
- package/dist/ui/api/content/[id]/ai/takeaways.js.map +1 -0
- package/dist/ui/api/content/[id]/contributors.d.ts +4 -0
- package/dist/ui/api/content/[id]/contributors.d.ts.map +1 -0
- package/dist/ui/api/content/[id]/contributors.js +8 -0
- package/dist/ui/api/content/[id]/contributors.js.map +1 -0
- package/dist/ui/api/content/[id]/preview-token.d.ts +3 -0
- package/dist/ui/api/content/[id]/preview-token.d.ts.map +1 -0
- package/dist/ui/api/content/[id]/preview-token.js +16 -0
- package/dist/ui/api/content/[id]/preview-token.js.map +1 -0
- package/dist/ui/api/content/[id]/revisions/[rev]/restore.d.ts +3 -0
- package/dist/ui/api/content/[id]/revisions/[rev]/restore.d.ts.map +1 -0
- package/dist/ui/api/content/[id]/revisions/[rev]/restore.js +7 -0
- package/dist/ui/api/content/[id]/revisions/[rev]/restore.js.map +1 -0
- package/dist/ui/api/content/[id]/revisions/[rev].d.ts +3 -0
- package/dist/ui/api/content/[id]/revisions/[rev].d.ts.map +1 -0
- package/dist/ui/api/content/[id]/revisions/[rev].js +6 -0
- package/dist/ui/api/content/[id]/revisions/[rev].js.map +1 -0
- package/dist/ui/api/content/[id]/revisions.d.ts +3 -0
- package/dist/ui/api/content/[id]/revisions.d.ts.map +1 -0
- package/dist/ui/api/content/[id]/revisions.js +6 -0
- package/dist/ui/api/content/[id]/revisions.js.map +1 -0
- package/dist/ui/api/content/[id]/seo-score.d.ts +3 -0
- package/dist/ui/api/content/[id]/seo-score.d.ts.map +1 -0
- package/dist/ui/api/content/[id]/seo-score.js +7 -0
- package/dist/ui/api/content/[id]/seo-score.js.map +1 -0
- package/dist/ui/api/content/[id].d.ts +5 -0
- package/dist/ui/api/content/[id].d.ts.map +1 -0
- package/dist/ui/api/content/[id].js +17 -0
- package/dist/ui/api/content/[id].js.map +1 -0
- package/dist/ui/api/content/bulk.d.ts +3 -0
- package/dist/ui/api/content/bulk.d.ts.map +1 -0
- package/dist/ui/api/content/bulk.js +7 -0
- package/dist/ui/api/content/bulk.js.map +1 -0
- package/dist/ui/api/content/counts.d.ts +3 -0
- package/dist/ui/api/content/counts.d.ts.map +1 -0
- package/dist/ui/api/content/counts.js +8 -0
- package/dist/ui/api/content/counts.js.map +1 -0
- package/dist/ui/api/content/foundry-callback.d.ts +3 -0
- package/dist/ui/api/content/foundry-callback.d.ts.map +1 -0
- package/dist/ui/api/content/foundry-callback.js +8 -0
- package/dist/ui/api/content/foundry-callback.js.map +1 -0
- package/dist/ui/api/content/import/confirm.d.ts +3 -0
- package/dist/ui/api/content/import/confirm.d.ts.map +1 -0
- package/dist/ui/api/content/import/confirm.js +11 -0
- package/dist/ui/api/content/import/confirm.js.map +1 -0
- package/dist/ui/api/content/import/parse.d.ts +3 -0
- package/dist/ui/api/content/import/parse.d.ts.map +1 -0
- package/dist/ui/api/content/import/parse.js +11 -0
- package/dist/ui/api/content/import/parse.js.map +1 -0
- package/dist/ui/api/content/insights-ingest.d.ts +3 -0
- package/dist/ui/api/content/insights-ingest.d.ts.map +1 -0
- package/dist/ui/api/content/insights-ingest.js +15 -0
- package/dist/ui/api/content/insights-ingest.js.map +1 -0
- package/dist/ui/api/content-insights/dismiss.d.ts +3 -0
- package/dist/ui/api/content-insights/dismiss.d.ts.map +1 -0
- package/dist/ui/api/content-insights/dismiss.js +16 -0
- package/dist/ui/api/content-insights/dismiss.js.map +1 -0
- package/dist/ui/api/content-insights/index.d.ts +3 -0
- package/dist/ui/api/content-insights/index.d.ts.map +1 -0
- package/dist/ui/api/content-insights/index.js +12 -0
- package/dist/ui/api/content-insights/index.js.map +1 -0
- package/dist/ui/api/content-insights/undismiss.d.ts +3 -0
- package/dist/ui/api/content-insights/undismiss.d.ts.map +1 -0
- package/dist/ui/api/content-insights/undismiss.js +16 -0
- package/dist/ui/api/content-insights/undismiss.js.map +1 -0
- package/dist/ui/api/content.d.ts +4 -0
- package/dist/ui/api/content.d.ts.map +1 -0
- package/dist/ui/api/content.js +16 -0
- package/dist/ui/api/content.js.map +1 -0
- package/dist/ui/api/dashboard.d.ts +3 -0
- package/dist/ui/api/dashboard.d.ts.map +1 -0
- package/dist/ui/api/dashboard.js +22 -0
- package/dist/ui/api/dashboard.js.map +1 -0
- package/dist/ui/api/me.d.ts +3 -0
- package/dist/ui/api/me.d.ts.map +1 -0
- package/dist/ui/api/me.js +18 -0
- package/dist/ui/api/me.js.map +1 -0
- package/dist/ui/api/media/[id].d.ts +3 -0
- package/dist/ui/api/media/[id].d.ts.map +1 -0
- package/dist/ui/api/media/[id].js +17 -0
- package/dist/ui/api/media/[id].js.map +1 -0
- package/dist/ui/api/media/images.d.ts +3 -0
- package/dist/ui/api/media/images.d.ts.map +1 -0
- package/dist/ui/api/media/images.js +6 -0
- package/dist/ui/api/media/images.js.map +1 -0
- package/dist/ui/api/media/library/[id].d.ts +3 -0
- package/dist/ui/api/media/library/[id].d.ts.map +1 -0
- package/dist/ui/api/media/library/[id].js +6 -0
- package/dist/ui/api/media/library/[id].js.map +1 -0
- package/dist/ui/api/media/library.d.ts +3 -0
- package/dist/ui/api/media/library.d.ts.map +1 -0
- package/dist/ui/api/media/library.js +17 -0
- package/dist/ui/api/media/library.js.map +1 -0
- package/dist/ui/api/media/podcast/abort.d.ts +3 -0
- package/dist/ui/api/media/podcast/abort.d.ts.map +1 -0
- package/dist/ui/api/media/podcast/abort.js +4 -0
- package/dist/ui/api/media/podcast/abort.js.map +1 -0
- package/dist/ui/api/media/podcast/complete.d.ts +3 -0
- package/dist/ui/api/media/podcast/complete.d.ts.map +1 -0
- package/dist/ui/api/media/podcast/complete.js +4 -0
- package/dist/ui/api/media/podcast/complete.js.map +1 -0
- package/dist/ui/api/media/podcast/init.d.ts +3 -0
- package/dist/ui/api/media/podcast/init.d.ts.map +1 -0
- package/dist/ui/api/media/podcast/init.js +4 -0
- package/dist/ui/api/media/podcast/init.js.map +1 -0
- package/dist/ui/api/media/podcast/part.d.ts +3 -0
- package/dist/ui/api/media/podcast/part.d.ts.map +1 -0
- package/dist/ui/api/media/podcast/part.js +4 -0
- package/dist/ui/api/media/podcast/part.js.map +1 -0
- package/dist/ui/api/media/podcast.d.ts +3 -0
- package/dist/ui/api/media/podcast.d.ts.map +1 -0
- package/dist/ui/api/media/podcast.js +6 -0
- package/dist/ui/api/media/podcast.js.map +1 -0
- package/dist/ui/api/media/videos/abort.d.ts +3 -0
- package/dist/ui/api/media/videos/abort.d.ts.map +1 -0
- package/dist/ui/api/media/videos/abort.js +6 -0
- package/dist/ui/api/media/videos/abort.js.map +1 -0
- package/dist/ui/api/media/videos/complete.d.ts +3 -0
- package/dist/ui/api/media/videos/complete.d.ts.map +1 -0
- package/dist/ui/api/media/videos/complete.js +6 -0
- package/dist/ui/api/media/videos/complete.js.map +1 -0
- package/dist/ui/api/media/videos/init.d.ts +3 -0
- package/dist/ui/api/media/videos/init.d.ts.map +1 -0
- package/dist/ui/api/media/videos/init.js +6 -0
- package/dist/ui/api/media/videos/init.js.map +1 -0
- package/dist/ui/api/media/videos/part.d.ts +3 -0
- package/dist/ui/api/media/videos/part.d.ts.map +1 -0
- package/dist/ui/api/media/videos/part.js +6 -0
- package/dist/ui/api/media/videos/part.js.map +1 -0
- package/dist/ui/api/notifications.d.ts +4 -0
- package/dist/ui/api/notifications.d.ts.map +1 -0
- package/dist/ui/api/notifications.js +20 -0
- package/dist/ui/api/notifications.js.map +1 -0
- package/dist/ui/api/search.d.ts +3 -0
- package/dist/ui/api/search.d.ts.map +1 -0
- package/dist/ui/api/search.js +18 -0
- package/dist/ui/api/search.js.map +1 -0
- package/dist/ui/api/settings/api-keys/[id].d.ts +3 -0
- package/dist/ui/api/settings/api-keys/[id].d.ts.map +1 -0
- package/dist/ui/api/settings/api-keys/[id].js +22 -0
- package/dist/ui/api/settings/api-keys/[id].js.map +1 -0
- package/dist/ui/api/settings/api-keys.d.ts +4 -0
- package/dist/ui/api/settings/api-keys.d.ts.map +1 -0
- package/dist/ui/api/settings/api-keys.js +19 -0
- package/dist/ui/api/settings/api-keys.js.map +1 -0
- package/dist/ui/api/settings/domains.d.ts +3 -0
- package/dist/ui/api/settings/domains.d.ts.map +1 -0
- package/dist/ui/api/settings/domains.js +32 -0
- package/dist/ui/api/settings/domains.js.map +1 -0
- package/dist/ui/api/settings/integrations.d.ts +3 -0
- package/dist/ui/api/settings/integrations.d.ts.map +1 -0
- package/dist/ui/api/settings/integrations.js +32 -0
- package/dist/ui/api/settings/integrations.js.map +1 -0
- package/dist/ui/api/settings/members/[userId].d.ts +4 -0
- package/dist/ui/api/settings/members/[userId].d.ts.map +1 -0
- package/dist/ui/api/settings/members/[userId].js +26 -0
- package/dist/ui/api/settings/members/[userId].js.map +1 -0
- package/dist/ui/api/settings/members/invite.d.ts +3 -0
- package/dist/ui/api/settings/members/invite.d.ts.map +1 -0
- package/dist/ui/api/settings/members/invite.js +21 -0
- package/dist/ui/api/settings/members/invite.js.map +1 -0
- package/dist/ui/api/settings/members.d.ts +3 -0
- package/dist/ui/api/settings/members.d.ts.map +1 -0
- package/dist/ui/api/settings/members.js +17 -0
- package/dist/ui/api/settings/members.js.map +1 -0
- package/dist/ui/api/settings/webhooks/[id]/test.d.ts +3 -0
- package/dist/ui/api/settings/webhooks/[id]/test.d.ts.map +1 -0
- package/dist/ui/api/settings/webhooks/[id]/test.js +24 -0
- package/dist/ui/api/settings/webhooks/[id]/test.js.map +1 -0
- package/dist/ui/api/settings/webhooks/[id].d.ts +3 -0
- package/dist/ui/api/settings/webhooks/[id].d.ts.map +1 -0
- package/dist/ui/api/settings/webhooks/[id].js +23 -0
- package/dist/ui/api/settings/webhooks/[id].js.map +1 -0
- package/dist/ui/api/settings/webhooks.d.ts +4 -0
- package/dist/ui/api/settings/webhooks.d.ts.map +1 -0
- package/dist/ui/api/settings/webhooks.js +21 -0
- package/dist/ui/api/settings/webhooks.js.map +1 -0
- package/dist/ui/api/subscriptions.d.ts +4 -0
- package/dist/ui/api/subscriptions.d.ts.map +1 -0
- package/dist/ui/api/subscriptions.js +47 -0
- package/dist/ui/api/subscriptions.js.map +1 -0
- package/dist/ui/api/tags/[id].d.ts +4 -0
- package/dist/ui/api/tags/[id].d.ts.map +1 -0
- package/dist/ui/api/tags/[id].js +18 -0
- package/dist/ui/api/tags/[id].js.map +1 -0
- package/dist/ui/api/tags/index.d.ts +4 -0
- package/dist/ui/api/tags/index.d.ts.map +1 -0
- package/dist/ui/api/tags/index.js +13 -0
- package/dist/ui/api/tags/index.js.map +1 -0
- package/dist/ui/api/topics/[id].d.ts +3 -0
- package/dist/ui/api/topics/[id].d.ts.map +1 -0
- package/dist/ui/api/topics/[id].js +15 -0
- package/dist/ui/api/topics/[id].js.map +1 -0
- package/dist/ui/api/topics/index.d.ts +4 -0
- package/dist/ui/api/topics/index.d.ts.map +1 -0
- package/dist/ui/api/topics/index.js +12 -0
- package/dist/ui/api/topics/index.js.map +1 -0
- package/dist/ui/api/workspace-settings.d.ts +4 -0
- package/dist/ui/api/workspace-settings.d.ts.map +1 -0
- package/dist/ui/api/workspace-settings.js +20 -0
- package/dist/ui/api/workspace-settings.js.map +1 -0
- package/dist/ui/client/boot-state.d.ts +15 -0
- package/dist/ui/client/boot-state.d.ts.map +1 -0
- package/dist/ui/client/boot-state.js +36 -0
- package/dist/ui/client/boot-state.js.map +1 -0
- package/dist/ui/client/mount.d.ts +3 -0
- package/dist/ui/client/mount.d.ts.map +1 -0
- package/dist/ui/client/mount.js +37 -0
- package/dist/ui/client/mount.js.map +1 -0
- package/dist/ui/commands.d.ts +23 -0
- package/dist/ui/commands.d.ts.map +1 -0
- package/dist/ui/commands.js +48 -0
- package/dist/ui/commands.js.map +1 -0
- package/dist/ui/components/CmsApp.d.ts +16 -0
- package/dist/ui/components/CmsApp.d.ts.map +1 -0
- package/dist/ui/components/CmsApp.js +74 -0
- package/dist/ui/components/CmsApp.js.map +1 -0
- package/dist/ui/components/CommandPalette.d.ts +7 -0
- package/dist/ui/components/CommandPalette.d.ts.map +1 -0
- package/dist/ui/components/CommandPalette.js +61 -0
- package/dist/ui/components/CommandPalette.js.map +1 -0
- package/dist/ui/components/NoAccessScreen.d.ts +2 -0
- package/dist/ui/components/NoAccessScreen.d.ts.map +1 -0
- package/dist/ui/components/NoAccessScreen.js +42 -0
- package/dist/ui/components/NoAccessScreen.js.map +1 -0
- package/dist/ui/components/ShareModal.d.ts +27 -0
- package/dist/ui/components/ShareModal.d.ts.map +1 -0
- package/dist/ui/components/ShareModal.js +208 -0
- package/dist/ui/components/ShareModal.js.map +1 -0
- package/dist/ui/components/SharePickers.d.ts +39 -0
- package/dist/ui/components/SharePickers.d.ts.map +1 -0
- package/dist/ui/components/SharePickers.js +352 -0
- package/dist/ui/components/SharePickers.js.map +1 -0
- package/dist/ui/components/ShareStatsPanel.d.ts +22 -0
- package/dist/ui/components/ShareStatsPanel.d.ts.map +1 -0
- package/dist/ui/components/ShareStatsPanel.js +317 -0
- package/dist/ui/components/ShareStatsPanel.js.map +1 -0
- package/dist/ui/components/Sidebar.d.ts +7 -0
- package/dist/ui/components/Sidebar.d.ts.map +1 -0
- package/dist/ui/components/Sidebar.js +20 -0
- package/dist/ui/components/Sidebar.js.map +1 -0
- package/dist/ui/components/SiteSwitcher.d.ts +4 -0
- package/dist/ui/components/SiteSwitcher.d.ts.map +1 -0
- package/dist/ui/components/SiteSwitcher.js +35 -0
- package/dist/ui/components/SiteSwitcher.js.map +1 -0
- package/dist/ui/components/Topbar.d.ts +9 -0
- package/dist/ui/components/Topbar.d.ts.map +1 -0
- package/dist/ui/components/Topbar.js +20 -0
- package/dist/ui/components/Topbar.js.map +1 -0
- package/dist/ui/editor/AiAssistPanel.d.ts +8 -0
- package/dist/ui/editor/AiAssistPanel.d.ts.map +1 -0
- package/dist/ui/editor/AiAssistPanel.js +221 -0
- package/dist/ui/editor/AiAssistPanel.js.map +1 -0
- package/dist/ui/editor/ContentForm.d.ts +46 -0
- package/dist/ui/editor/ContentForm.d.ts.map +1 -0
- package/dist/ui/editor/ContentForm.js +821 -0
- package/dist/ui/editor/ContentForm.js.map +1 -0
- package/dist/ui/editor/Rte.d.ts +16 -0
- package/dist/ui/editor/Rte.d.ts.map +1 -0
- package/dist/ui/editor/Rte.js +272 -0
- package/dist/ui/editor/Rte.js.map +1 -0
- package/dist/ui/editor/ai-assist.d.ts +43 -0
- package/dist/ui/editor/ai-assist.d.ts.map +1 -0
- package/dist/ui/editor/ai-assist.js +114 -0
- package/dist/ui/editor/ai-assist.js.map +1 -0
- package/dist/ui/editor/autosave.d.ts +18 -0
- package/dist/ui/editor/autosave.d.ts.map +1 -0
- package/dist/ui/editor/autosave.js +23 -0
- package/dist/ui/editor/autosave.js.map +1 -0
- package/dist/ui/editor/content-payload.d.ts +19 -0
- package/dist/ui/editor/content-payload.d.ts.map +1 -0
- package/dist/ui/editor/content-payload.js +97 -0
- package/dist/ui/editor/content-payload.js.map +1 -0
- package/dist/ui/editor/editor-media-upload.d.ts +6 -0
- package/dist/ui/editor/editor-media-upload.d.ts.map +1 -0
- package/dist/ui/editor/editor-media-upload.js +20 -0
- package/dist/ui/editor/editor-media-upload.js.map +1 -0
- package/dist/ui/editor/serialize.d.ts +6 -0
- package/dist/ui/editor/serialize.d.ts.map +1 -0
- package/dist/ui/editor/serialize.js +479 -0
- package/dist/ui/editor/serialize.js.map +1 -0
- package/dist/ui/editor/tweet-embed.d.ts +4 -0
- package/dist/ui/editor/tweet-embed.d.ts.map +1 -0
- package/dist/ui/editor/tweet-embed.js +49 -0
- package/dist/ui/editor/tweet-embed.js.map +1 -0
- package/dist/ui/hash-router.d.ts +5 -0
- package/dist/ui/hash-router.d.ts.map +1 -0
- package/dist/ui/hash-router.js +25 -0
- package/dist/ui/hash-router.js.map +1 -0
- package/dist/ui/icons.d.ts +32 -0
- package/dist/ui/icons.d.ts.map +1 -0
- package/dist/ui/icons.js +86 -0
- package/dist/ui/icons.js.map +1 -0
- package/dist/ui/inspector/Field.d.ts +12 -0
- package/dist/ui/inspector/Field.d.ts.map +1 -0
- package/dist/ui/inspector/Field.js +8 -0
- package/dist/ui/inspector/Field.js.map +1 -0
- package/dist/ui/inspector/FoundryTab.d.ts +9 -0
- package/dist/ui/inspector/FoundryTab.d.ts.map +1 -0
- package/dist/ui/inspector/FoundryTab.js +362 -0
- package/dist/ui/inspector/FoundryTab.js.map +1 -0
- package/dist/ui/inspector/HistoryTab.d.ts +7 -0
- package/dist/ui/inspector/HistoryTab.d.ts.map +1 -0
- package/dist/ui/inspector/HistoryTab.js +289 -0
- package/dist/ui/inspector/HistoryTab.js.map +1 -0
- package/dist/ui/inspector/Inspector.d.ts +13 -0
- package/dist/ui/inspector/Inspector.d.ts.map +1 -0
- package/dist/ui/inspector/Inspector.js +163 -0
- package/dist/ui/inspector/Inspector.js.map +1 -0
- package/dist/ui/inspector/OrganizeTab.d.ts +15 -0
- package/dist/ui/inspector/OrganizeTab.d.ts.map +1 -0
- package/dist/ui/inspector/OrganizeTab.js +319 -0
- package/dist/ui/inspector/OrganizeTab.js.map +1 -0
- package/dist/ui/inspector/PublishTab.d.ts +18 -0
- package/dist/ui/inspector/PublishTab.d.ts.map +1 -0
- package/dist/ui/inspector/PublishTab.js +339 -0
- package/dist/ui/inspector/PublishTab.js.map +1 -0
- package/dist/ui/inspector/Section.d.ts +10 -0
- package/dist/ui/inspector/Section.d.ts.map +1 -0
- package/dist/ui/inspector/Section.js +40 -0
- package/dist/ui/inspector/Section.js.map +1 -0
- package/dist/ui/inspector/SeoTab.d.ts +19 -0
- package/dist/ui/inspector/SeoTab.d.ts.map +1 -0
- package/dist/ui/inspector/SeoTab.js +328 -0
- package/dist/ui/inspector/SeoTab.js.map +1 -0
- package/dist/ui/inspector/foundry-stages.d.ts +36 -0
- package/dist/ui/inspector/foundry-stages.d.ts.map +1 -0
- package/dist/ui/inspector/foundry-stages.js +101 -0
- package/dist/ui/inspector/foundry-stages.js.map +1 -0
- package/dist/ui/inspector/inspector-data.d.ts +80 -0
- package/dist/ui/inspector/inspector-data.d.ts.map +1 -0
- package/dist/ui/inspector/inspector-data.js +172 -0
- package/dist/ui/inspector/inspector-data.js.map +1 -0
- package/dist/ui/inspector/organize-data.d.ts +23 -0
- package/dist/ui/inspector/organize-data.d.ts.map +1 -0
- package/dist/ui/inspector/organize-data.js +28 -0
- package/dist/ui/inspector/organize-data.js.map +1 -0
- package/dist/ui/inspector/revision-diff.d.ts +49 -0
- package/dist/ui/inspector/revision-diff.d.ts.map +1 -0
- package/dist/ui/inspector/revision-diff.js +166 -0
- package/dist/ui/inspector/revision-diff.js.map +1 -0
- package/dist/ui/inspector/seo-helpers.d.ts +37 -0
- package/dist/ui/inspector/seo-helpers.d.ts.map +1 -0
- package/dist/ui/inspector/seo-helpers.js +37 -0
- package/dist/ui/inspector/seo-helpers.js.map +1 -0
- package/dist/ui/inspector/tab-visibility.d.ts +14 -0
- package/dist/ui/inspector/tab-visibility.d.ts.map +1 -0
- package/dist/ui/inspector/tab-visibility.js +28 -0
- package/dist/ui/inspector/tab-visibility.js.map +1 -0
- package/dist/ui/nav.d.ts +16 -0
- package/dist/ui/nav.d.ts.map +1 -0
- package/dist/ui/nav.js +33 -0
- package/dist/ui/nav.js.map +1 -0
- package/dist/ui/pages/admin.astro +32 -0
- package/dist/ui/preview/draft-page.d.ts +37 -0
- package/dist/ui/preview/draft-page.d.ts.map +1 -0
- package/dist/ui/preview/draft-page.js +212 -0
- package/dist/ui/preview/draft-page.js.map +1 -0
- package/dist/ui/preview/preview-layout.d.ts +23 -0
- package/dist/ui/preview/preview-layout.d.ts.map +1 -0
- package/dist/ui/preview/preview-layout.js +30 -0
- package/dist/ui/preview/preview-layout.js.map +1 -0
- package/dist/ui/screens/AnalyticsScreen.d.ts +2 -0
- package/dist/ui/screens/AnalyticsScreen.d.ts.map +1 -0
- package/dist/ui/screens/AnalyticsScreen.js +408 -0
- package/dist/ui/screens/AnalyticsScreen.js.map +1 -0
- package/dist/ui/screens/AuthorsScreen.d.ts +2 -0
- package/dist/ui/screens/AuthorsScreen.d.ts.map +1 -0
- package/dist/ui/screens/AuthorsScreen.js +225 -0
- package/dist/ui/screens/AuthorsScreen.js.map +1 -0
- package/dist/ui/screens/CalendarScreen.d.ts +6 -0
- package/dist/ui/screens/CalendarScreen.d.ts.map +1 -0
- package/dist/ui/screens/CalendarScreen.js +327 -0
- package/dist/ui/screens/CalendarScreen.js.map +1 -0
- package/dist/ui/screens/ContentInsightsScreen.d.ts +2 -0
- package/dist/ui/screens/ContentInsightsScreen.d.ts.map +1 -0
- package/dist/ui/screens/ContentInsightsScreen.js +129 -0
- package/dist/ui/screens/ContentInsightsScreen.js.map +1 -0
- package/dist/ui/screens/ContentRoute.d.ts +2 -0
- package/dist/ui/screens/ContentRoute.d.ts.map +1 -0
- package/dist/ui/screens/ContentRoute.js +32 -0
- package/dist/ui/screens/ContentRoute.js.map +1 -0
- package/dist/ui/screens/DashboardScreen.d.ts +6 -0
- package/dist/ui/screens/DashboardScreen.d.ts.map +1 -0
- package/dist/ui/screens/DashboardScreen.js +273 -0
- package/dist/ui/screens/DashboardScreen.js.map +1 -0
- package/dist/ui/screens/EditorScreen.d.ts +7 -0
- package/dist/ui/screens/EditorScreen.d.ts.map +1 -0
- package/dist/ui/screens/EditorScreen.js +426 -0
- package/dist/ui/screens/EditorScreen.js.map +1 -0
- package/dist/ui/screens/LibraryScreen.d.ts +6 -0
- package/dist/ui/screens/LibraryScreen.d.ts.map +1 -0
- package/dist/ui/screens/LibraryScreen.js +580 -0
- package/dist/ui/screens/LibraryScreen.js.map +1 -0
- package/dist/ui/screens/MediaScreen.d.ts +2 -0
- package/dist/ui/screens/MediaScreen.d.ts.map +1 -0
- package/dist/ui/screens/MediaScreen.js +173 -0
- package/dist/ui/screens/MediaScreen.js.map +1 -0
- package/dist/ui/screens/SettingsScreen.d.ts +4 -0
- package/dist/ui/screens/SettingsScreen.d.ts.map +1 -0
- package/dist/ui/screens/SettingsScreen.js +751 -0
- package/dist/ui/screens/SettingsScreen.js.map +1 -0
- package/dist/ui/screens/SocialShareScreen.d.ts +2 -0
- package/dist/ui/screens/SocialShareScreen.d.ts.map +1 -0
- package/dist/ui/screens/SocialShareScreen.js +224 -0
- package/dist/ui/screens/SocialShareScreen.js.map +1 -0
- package/dist/ui/screens/SubscriptionsScreen.d.ts +2 -0
- package/dist/ui/screens/SubscriptionsScreen.d.ts.map +1 -0
- package/dist/ui/screens/SubscriptionsScreen.js +441 -0
- package/dist/ui/screens/SubscriptionsScreen.js.map +1 -0
- package/dist/ui/screens/TopicsScreen.d.ts +2 -0
- package/dist/ui/screens/TopicsScreen.d.ts.map +1 -0
- package/dist/ui/screens/TopicsScreen.js +360 -0
- package/dist/ui/screens/TopicsScreen.js.map +1 -0
- package/dist/ui/screens/analytics-data.d.ts +19 -0
- package/dist/ui/screens/analytics-data.d.ts.map +1 -0
- package/dist/ui/screens/analytics-data.js +42 -0
- package/dist/ui/screens/analytics-data.js.map +1 -0
- package/dist/ui/screens/calendar-data.d.ts +45 -0
- package/dist/ui/screens/calendar-data.d.ts.map +1 -0
- package/dist/ui/screens/calendar-data.js +70 -0
- package/dist/ui/screens/calendar-data.js.map +1 -0
- package/dist/ui/screens/content-insights-data.d.ts +54 -0
- package/dist/ui/screens/content-insights-data.d.ts.map +1 -0
- package/dist/ui/screens/content-insights-data.js +82 -0
- package/dist/ui/screens/content-insights-data.js.map +1 -0
- package/dist/ui/screens/content-view.d.ts +21 -0
- package/dist/ui/screens/content-view.d.ts.map +1 -0
- package/dist/ui/screens/content-view.js +17 -0
- package/dist/ui/screens/content-view.js.map +1 -0
- package/dist/ui/screens/library-data.d.ts +76 -0
- package/dist/ui/screens/library-data.d.ts.map +1 -0
- package/dist/ui/screens/library-data.js +116 -0
- package/dist/ui/screens/library-data.js.map +1 -0
- package/dist/ui/screens/media-upload.d.ts +19 -0
- package/dist/ui/screens/media-upload.d.ts.map +1 -0
- package/dist/ui/screens/media-upload.js +187 -0
- package/dist/ui/screens/media-upload.js.map +1 -0
- package/dist/ui/screens/public-url.d.ts +24 -0
- package/dist/ui/screens/public-url.d.ts.map +1 -0
- package/dist/ui/screens/public-url.js +74 -0
- package/dist/ui/screens/public-url.js.map +1 -0
- package/dist/ui/screens/registry.d.ts +3 -0
- package/dist/ui/screens/registry.d.ts.map +1 -0
- package/dist/ui/screens/registry.js +38 -0
- package/dist/ui/screens/registry.js.map +1 -0
- package/dist/ui/screens/render-state.d.ts +105 -0
- package/dist/ui/screens/render-state.d.ts.map +1 -0
- package/dist/ui/screens/render-state.js +127 -0
- package/dist/ui/screens/render-state.js.map +1 -0
- package/dist/ui/screens/settings-data.d.ts +55 -0
- package/dist/ui/screens/settings-data.d.ts.map +1 -0
- package/dist/ui/screens/settings-data.js +89 -0
- package/dist/ui/screens/settings-data.js.map +1 -0
- package/dist/ui/screens/settings-panels-data.d.ts +58 -0
- package/dist/ui/screens/settings-panels-data.d.ts.map +1 -0
- package/dist/ui/screens/settings-panels-data.js +88 -0
- package/dist/ui/screens/settings-panels-data.js.map +1 -0
- package/dist/ui/screens/social-share-data.d.ts +307 -0
- package/dist/ui/screens/social-share-data.d.ts.map +1 -0
- package/dist/ui/screens/social-share-data.js +447 -0
- package/dist/ui/screens/social-share-data.js.map +1 -0
- package/dist/ui/screens/topics-data.d.ts +38 -0
- package/dist/ui/screens/topics-data.d.ts.map +1 -0
- package/dist/ui/screens/topics-data.js +50 -0
- package/dist/ui/screens/topics-data.js.map +1 -0
- package/dist/ui/styles/broadsheet.css +394 -0
- package/dist/ui/theme.d.ts +23 -0
- package/dist/ui/theme.d.ts.map +1 -0
- package/dist/ui/theme.js +87 -0
- package/dist/ui/theme.js.map +1 -0
- package/dist/ui/tweaks.d.ts +7 -0
- package/dist/ui/tweaks.d.ts.map +1 -0
- package/dist/ui/tweaks.js +31 -0
- package/dist/ui/tweaks.js.map +1 -0
- package/dist/ui/use-hash-router.d.ts +6 -0
- package/dist/ui/use-hash-router.d.ts.map +1 -0
- package/dist/ui/use-hash-router.js +15 -0
- package/dist/ui/use-hash-router.js.map +1 -0
- package/dist/ui/use-tweaks.d.ts +6 -0
- package/dist/ui/use-tweaks.d.ts.map +1 -0
- package/dist/ui/use-tweaks.js +24 -0
- package/dist/ui/use-tweaks.js.map +1 -0
- package/dist/ui/workspace-context.d.ts +34 -0
- package/dist/ui/workspace-context.d.ts.map +1 -0
- package/dist/ui/workspace-context.js +32 -0
- package/dist/ui/workspace-context.js.map +1 -0
- package/migrations/0001_create_cms_tables.sql +200 -0
- package/migrations/0002_review_softdelete_board.sql +112 -0
- package/migrations/0003_content_contributors.sql +16 -0
- package/migrations/0004_seo_faq_columns.sql +6 -0
- package/migrations/0005_revision_delta_columns.sql +5 -0
- package/migrations/0006_processing_trigger_token.sql +4 -0
- package/migrations/0007_content_slug_redirects.sql +9 -0
- package/migrations/0008_workspace_settings.sql +17 -0
- package/migrations/0009_workspace_memberships.sql +36 -0
- package/migrations/0010_api_keys_webhooks.sql +21 -0
- package/migrations/0011_notifications_activity.sql +22 -0
- package/migrations/0012_content_imports.sql +10 -0
- package/migrations/0013_media_normalization.sql +4 -0
- package/migrations/0014_api_key_prefix.sql +3 -0
- package/migrations/0015_cms_topics.sql +7 -0
- package/migrations/0016_content_insights.sql +53 -0
- package/package.json +82 -0
- package/src/engine/activity-log.ts +39 -0
- package/src/engine/ai-prompts.ts +124 -0
- package/src/engine/ai-writeback.ts +62 -0
- package/src/engine/api-keys.ts +239 -0
- package/src/engine/content-insights.ts +198 -0
- package/src/engine/contributors.ts +95 -0
- package/src/engine/cron.ts +62 -0
- package/src/engine/d1.ts +29 -0
- package/src/engine/foundry-dispatch.ts +417 -0
- package/src/engine/import-parsers.ts +478 -0
- package/src/engine/index.ts +230 -0
- package/src/engine/invites.ts +271 -0
- package/src/engine/members.ts +216 -0
- package/src/engine/membership-rules.ts +63 -0
- package/src/engine/og-render.ts +59 -0
- package/src/engine/publish-guard.ts +123 -0
- package/src/engine/publisher.ts +1032 -0
- package/src/engine/revisions.ts +292 -0
- package/src/engine/sanitize.ts +183 -0
- package/src/engine/seed-membership.ts +92 -0
- package/src/engine/seo.ts +72 -0
- package/src/engine/slug-redirects.ts +34 -0
- package/src/engine/slug.ts +33 -0
- package/src/engine/soft-delete.ts +42 -0
- package/src/engine/tags.ts +95 -0
- package/src/engine/topics.ts +158 -0
- package/src/engine/url-guard.ts +136 -0
- package/src/engine/validator/checks/bare-url-not-autolinked.ts +78 -0
- package/src/engine/validator/checks/broken-footnote-label.ts +33 -0
- package/src/engine/validator/checks/double-encoded-entities.ts +46 -0
- package/src/engine/validator/checks/empty-alt-text.ts +35 -0
- package/src/engine/validator/checks/heading-hierarchy-skip.ts +33 -0
- package/src/engine/validator/checks/html-comment-leak.ts +58 -0
- package/src/engine/validator/checks/iframe-missing-dims-and-wrapper.ts +34 -0
- package/src/engine/validator/checks/invisible-control-chars.ts +58 -0
- package/src/engine/validator/checks/paywall-marker-leak.ts +43 -0
- package/src/engine/validator/checks/raw-block-html.ts +65 -0
- package/src/engine/validator/checks/stale-body-html.ts +39 -0
- package/src/engine/validator/checks/unresolved-footnote-anchor.ts +61 -0
- package/src/engine/validator/checks/word-gdocs-paste-artifacts.ts +72 -0
- package/src/engine/validator/index.ts +385 -0
- package/src/engine/validator/scan.ts +103 -0
- package/src/engine/validator/types.ts +114 -0
- package/src/engine/webhook-signer.ts +139 -0
- package/src/engine/webhooks.ts +224 -0
- package/src/index.ts +79 -0
- package/src/integration/index.ts +298 -0
- package/src/integration/options.ts +30 -0
- package/src/integration/vite-plugin.ts +37 -0
- package/src/providers/index.ts +2 -0
- package/src/providers/null.ts +160 -0
- package/src/providers/types.ts +284 -0
- package/src/routes/ai.ts +461 -0
- package/src/routes/analytics.ts +78 -0
- package/src/routes/api-keys.ts +133 -0
- package/src/routes/authors.ts +282 -0
- package/src/routes/authz-matrix.ts +239 -0
- package/src/routes/calendar.ts +127 -0
- package/src/routes/config.ts +99 -0
- package/src/routes/content-insights.ts +159 -0
- package/src/routes/content.ts +1753 -0
- package/src/routes/context.ts +146 -0
- package/src/routes/cron.ts +27 -0
- package/src/routes/dashboard.ts +174 -0
- package/src/routes/imports.ts +190 -0
- package/src/routes/index.ts +295 -0
- package/src/routes/media-lib.ts +405 -0
- package/src/routes/media.ts +944 -0
- package/src/routes/preview.ts +182 -0
- package/src/routes/rbac-invites.ts +220 -0
- package/src/routes/rbac.ts +155 -0
- package/src/routes/shell.ts +163 -0
- package/src/routes/subscriptions.ts +167 -0
- package/src/routes/tags.ts +93 -0
- package/src/routes/topics.ts +58 -0
- package/src/routes/webhooks.ts +233 -0
- package/src/schema/index.ts +45 -0
- package/src/schema/insights-ingest.ts +126 -0
- package/src/schema/migrations.ts +599 -0
- package/src/schema/tables.ts +59 -0
- package/src/schema/types.ts +576 -0
- package/src/ui/api/_authz.ts +100 -0
- package/src/ui/api/_content-config.ts +75 -0
- package/src/ui/api/activity.ts +33 -0
- package/src/ui/api/analytics.ts +42 -0
- package/src/ui/api/authors/[id].ts +23 -0
- package/src/ui/api/authors.ts +19 -0
- package/src/ui/api/calendar.ts +21 -0
- package/src/ui/api/content/[id]/ai/headlines.ts +10 -0
- package/src/ui/api/content/[id]/ai/meta-description.ts +11 -0
- package/src/ui/api/content/[id]/ai/og-image.ts +10 -0
- package/src/ui/api/content/[id]/ai/proofread.ts +10 -0
- package/src/ui/api/content/[id]/ai/takeaways.ts +11 -0
- package/src/ui/api/content/[id]/contributors.ts +13 -0
- package/src/ui/api/content/[id]/preview-token.ts +21 -0
- package/src/ui/api/content/[id]/revisions/[rev]/restore.ts +12 -0
- package/src/ui/api/content/[id]/revisions/[rev].ts +11 -0
- package/src/ui/api/content/[id]/revisions.ts +9 -0
- package/src/ui/api/content/[id]/seo-score.ts +10 -0
- package/src/ui/api/content/[id].ts +23 -0
- package/src/ui/api/content/bulk.ts +10 -0
- package/src/ui/api/content/counts.ts +11 -0
- package/src/ui/api/content/foundry-callback.ts +11 -0
- package/src/ui/api/content/import/confirm.ts +16 -0
- package/src/ui/api/content/import/parse.ts +16 -0
- package/src/ui/api/content/insights-ingest.ts +24 -0
- package/src/ui/api/content-insights/dismiss.ts +23 -0
- package/src/ui/api/content-insights/index.ts +21 -0
- package/src/ui/api/content-insights/undismiss.ts +23 -0
- package/src/ui/api/content.ts +21 -0
- package/src/ui/api/dashboard.ts +28 -0
- package/src/ui/api/me.ts +23 -0
- package/src/ui/api/media/[id].ts +22 -0
- package/src/ui/api/media/images.ts +9 -0
- package/src/ui/api/media/library/[id].ts +9 -0
- package/src/ui/api/media/library.ts +22 -0
- package/src/ui/api/media/podcast/abort.ts +6 -0
- package/src/ui/api/media/podcast/complete.ts +6 -0
- package/src/ui/api/media/podcast/init.ts +6 -0
- package/src/ui/api/media/podcast/part.ts +6 -0
- package/src/ui/api/media/podcast.ts +9 -0
- package/src/ui/api/media/videos/abort.ts +9 -0
- package/src/ui/api/media/videos/complete.ts +9 -0
- package/src/ui/api/media/videos/init.ts +9 -0
- package/src/ui/api/media/videos/part.ts +9 -0
- package/src/ui/api/notifications.ts +26 -0
- package/src/ui/api/search.ts +23 -0
- package/src/ui/api/settings/api-keys/[id].ts +28 -0
- package/src/ui/api/settings/api-keys.ts +25 -0
- package/src/ui/api/settings/domains.ts +37 -0
- package/src/ui/api/settings/integrations.ts +40 -0
- package/src/ui/api/settings/members/[userId].ts +33 -0
- package/src/ui/api/settings/members/invite.ts +27 -0
- package/src/ui/api/settings/members.ts +23 -0
- package/src/ui/api/settings/webhooks/[id]/test.ts +30 -0
- package/src/ui/api/settings/webhooks/[id].ts +29 -0
- package/src/ui/api/settings/webhooks.ts +27 -0
- package/src/ui/api/subscriptions.ts +56 -0
- package/src/ui/api/tags/[id].ts +24 -0
- package/src/ui/api/tags/index.ts +18 -0
- package/src/ui/api/topics/[id].ts +20 -0
- package/src/ui/api/topics/index.ts +17 -0
- package/src/ui/api/workspace-settings.ts +26 -0
- package/src/ui/client/boot-state.ts +42 -0
- package/src/ui/client/mount.tsx +41 -0
- package/src/ui/commands.ts +62 -0
- package/src/ui/components/CmsApp.tsx +149 -0
- package/src/ui/components/CommandPalette.tsx +118 -0
- package/src/ui/components/NoAccessScreen.tsx +79 -0
- package/src/ui/components/ShareModal.tsx +650 -0
- package/src/ui/components/SharePickers.tsx +790 -0
- package/src/ui/components/ShareStatsPanel.tsx +721 -0
- package/src/ui/components/Sidebar.tsx +86 -0
- package/src/ui/components/SiteSwitcher.tsx +100 -0
- package/src/ui/components/Topbar.tsx +93 -0
- package/src/ui/editor/AiAssistPanel.tsx +407 -0
- package/src/ui/editor/ContentForm.tsx +1462 -0
- package/src/ui/editor/Rte.tsx +382 -0
- package/src/ui/editor/ai-assist.ts +139 -0
- package/src/ui/editor/autosave.ts +36 -0
- package/src/ui/editor/content-payload.ts +125 -0
- package/src/ui/editor/editor-media-upload.ts +26 -0
- package/src/ui/editor/serialize.ts +522 -0
- package/src/ui/editor/tweet-embed.ts +60 -0
- package/src/ui/hash-router.ts +30 -0
- package/src/ui/icons.tsx +208 -0
- package/src/ui/inspector/Field.tsx +30 -0
- package/src/ui/inspector/FoundryTab.tsx +613 -0
- package/src/ui/inspector/HistoryTab.tsx +482 -0
- package/src/ui/inspector/Inspector.tsx +328 -0
- package/src/ui/inspector/OrganizeTab.tsx +534 -0
- package/src/ui/inspector/PublishTab.tsx +626 -0
- package/src/ui/inspector/Section.tsx +81 -0
- package/src/ui/inspector/SeoTab.tsx +573 -0
- package/src/ui/inspector/foundry-stages.ts +140 -0
- package/src/ui/inspector/inspector-data.ts +232 -0
- package/src/ui/inspector/organize-data.ts +51 -0
- package/src/ui/inspector/revision-diff.ts +213 -0
- package/src/ui/inspector/seo-helpers.ts +71 -0
- package/src/ui/inspector/tab-visibility.ts +37 -0
- package/src/ui/nav.ts +48 -0
- package/src/ui/pages/admin.astro +32 -0
- package/src/ui/preview/draft-page.tsx +395 -0
- package/src/ui/preview/preview-layout.ts +49 -0
- package/src/ui/screens/AnalyticsScreen.tsx +938 -0
- package/src/ui/screens/AuthorsScreen.tsx +524 -0
- package/src/ui/screens/CalendarScreen.tsx +694 -0
- package/src/ui/screens/ContentInsightsScreen.tsx +417 -0
- package/src/ui/screens/ContentRoute.tsx +35 -0
- package/src/ui/screens/DashboardScreen.tsx +654 -0
- package/src/ui/screens/EditorScreen.tsx +673 -0
- package/src/ui/screens/LibraryScreen.tsx +1350 -0
- package/src/ui/screens/MediaScreen.tsx +357 -0
- package/src/ui/screens/SettingsScreen.tsx +1841 -0
- package/src/ui/screens/SocialShareScreen.tsx +670 -0
- package/src/ui/screens/SubscriptionsScreen.tsx +1240 -0
- package/src/ui/screens/TopicsScreen.tsx +912 -0
- package/src/ui/screens/analytics-data.ts +68 -0
- package/src/ui/screens/calendar-data.ts +126 -0
- package/src/ui/screens/content-insights-data.ts +127 -0
- package/src/ui/screens/content-view.ts +30 -0
- package/src/ui/screens/library-data.ts +177 -0
- package/src/ui/screens/media-upload.ts +283 -0
- package/src/ui/screens/public-url.ts +81 -0
- package/src/ui/screens/registry.tsx +53 -0
- package/src/ui/screens/render-state.ts +228 -0
- package/src/ui/screens/settings-data.ts +140 -0
- package/src/ui/screens/settings-panels-data.ts +142 -0
- package/src/ui/screens/social-share-data.ts +753 -0
- package/src/ui/screens/topics-data.ts +75 -0
- package/src/ui/styles/broadsheet.css +394 -0
- package/src/ui/theme.ts +104 -0
- package/src/ui/tweaks.ts +37 -0
- package/src/ui/use-hash-router.ts +17 -0
- package/src/ui/use-tweaks.ts +31 -0
- package/src/ui/workspace-context.tsx +62 -0
- package/src/virtual.d.ts +4 -0
|
@@ -0,0 +1,1374 @@
|
|
|
1
|
+
// Content route handlers (WS7-07). Ported from fulcrum-labs/fronts
|
|
2
|
+
// src/pages/api/v1/publisher/content/index.ts + [id].ts (inventory §3:
|
|
3
|
+
// extract). Calls the already-extracted engine functions (createContent,
|
|
4
|
+
// updateContentItem, scheduleContent, publishContent, unpublishContent,
|
|
5
|
+
// duplicateContentItem, createRevision, evaluateContentBodyForPublish).
|
|
6
|
+
//
|
|
7
|
+
// Two Fronts couplings are removed:
|
|
8
|
+
// - authz: injected via config.authz.requirePublisher (NOT context.locals).
|
|
9
|
+
// - the Foundry article-takeaways dispatch (lib/foundry/article-takeaways.ts)
|
|
10
|
+
// is bead-08 glue, NOT yet extracted into the engine. So instead of
|
|
11
|
+
// hard-importing it, the publish + regenerate-takeaways actions call an
|
|
12
|
+
// OPTIONAL injected `dispatchTakeaways` hook (config). A site that wires the
|
|
13
|
+
// Foundry dispatcher gets the exact Fronts behavior; a site that omits it
|
|
14
|
+
// simply publishes without queuing takeaways. This keeps the package free
|
|
15
|
+
// of the Foundry/Hermes media glue (which lands with bead 08 + OD-CMS7).
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
import { logActivity } from '../engine/activity-log.js';
|
|
18
|
+
import { listContributors, setContributors } from '../engine/contributors.js';
|
|
19
|
+
import { applyFoundryCallback, dispatchToFoundry, verifyHmac, } from '../engine/foundry-dispatch.js';
|
|
20
|
+
import { evaluateArticleBody } from '../engine/publish-guard.js';
|
|
21
|
+
import { createContent, createRevision, duplicateContentItem, evaluateContentBodyForPublish, getContentItem, getContentRelations, publishContent, scheduleContent, unpublishContent, updateContentItem, } from '../engine/publisher.js';
|
|
22
|
+
import { createRevisionWithDelta, getRevision, listRevisions, restoreRevision, } from '../engine/revisions.js';
|
|
23
|
+
import { recordSlugRedirect } from '../engine/slug-redirects.js';
|
|
24
|
+
import { softDeleteContent } from '../engine/soft-delete.js';
|
|
25
|
+
import { canPerformContentAction } from './authz-matrix.js';
|
|
26
|
+
import { resolveConfig } from './config.js';
|
|
27
|
+
import { json, requireId } from './context.js';
|
|
28
|
+
const PUBLISH_TIMEZONE_FALLBACK = 'Europe/Paris';
|
|
29
|
+
const LOCAL_DATE_TIME_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})$/;
|
|
30
|
+
/**
|
|
31
|
+
* Map a content `action` verb (the ?action= query value) to the capability the
|
|
32
|
+
* 5-role matrix gates it on. Lifecycle/destructive verbs require publish-level
|
|
33
|
+
* authority; status/duplicate/snapshot edits are writes; media-pipeline dispatch
|
|
34
|
+
* verbs require media_pipeline. publish on a video/podcast resolves to
|
|
35
|
+
* publish_video so the producer role can ship video.
|
|
36
|
+
*/
|
|
37
|
+
function mapActionToCapability(action, type) {
|
|
38
|
+
switch (action) {
|
|
39
|
+
case 'publish':
|
|
40
|
+
return type === 'video' || type === 'podcast' ? 'publish_video' : 'publish';
|
|
41
|
+
case 'unpublish':
|
|
42
|
+
case 'archive':
|
|
43
|
+
case 'unschedule':
|
|
44
|
+
return 'publish';
|
|
45
|
+
case 'schedule':
|
|
46
|
+
return 'schedule';
|
|
47
|
+
case 'trash':
|
|
48
|
+
return 'trash';
|
|
49
|
+
case 'dispatch-to-foundry':
|
|
50
|
+
case 'regenerate-takeaways':
|
|
51
|
+
case 'regenerate-faq':
|
|
52
|
+
return 'media';
|
|
53
|
+
default:
|
|
54
|
+
// duplicate / set-status / snapshot and any unknown verb → write-level.
|
|
55
|
+
return 'write';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const parseTypes = (value) => {
|
|
59
|
+
if (!value)
|
|
60
|
+
return undefined;
|
|
61
|
+
const allowed = ['article', 'video', 'podcast', 'newsletter'];
|
|
62
|
+
const parts = value
|
|
63
|
+
.split(',')
|
|
64
|
+
.map((item) => item.trim())
|
|
65
|
+
.filter(Boolean);
|
|
66
|
+
const filtered = parts.filter((part) => allowed.includes(part));
|
|
67
|
+
return filtered.length > 0 ? filtered : undefined;
|
|
68
|
+
};
|
|
69
|
+
const parseStatus = (value) => {
|
|
70
|
+
if (!value)
|
|
71
|
+
return undefined;
|
|
72
|
+
const allowed = ['draft', 'scheduled', 'review', 'published', 'archived'];
|
|
73
|
+
return allowed.includes(value) ? value : undefined;
|
|
74
|
+
};
|
|
75
|
+
const parseCursor = (cursor) => {
|
|
76
|
+
if (!cursor)
|
|
77
|
+
return null;
|
|
78
|
+
const [timestamp, id] = cursor.split(':');
|
|
79
|
+
const updatedAt = Number(timestamp);
|
|
80
|
+
if (!Number.isFinite(updatedAt) || !id)
|
|
81
|
+
return null;
|
|
82
|
+
return { updatedAt, id };
|
|
83
|
+
};
|
|
84
|
+
function normalizeUnixSeconds(value) {
|
|
85
|
+
if (value === null || value === undefined || value === '')
|
|
86
|
+
return null;
|
|
87
|
+
if (typeof value === 'number') {
|
|
88
|
+
if (!Number.isFinite(value))
|
|
89
|
+
return null;
|
|
90
|
+
return value > 1e11 ? Math.floor(value / 1000) : value;
|
|
91
|
+
}
|
|
92
|
+
if (typeof value !== 'string')
|
|
93
|
+
return null;
|
|
94
|
+
const trimmed = value.trim();
|
|
95
|
+
if (!trimmed)
|
|
96
|
+
return null;
|
|
97
|
+
const numeric = Number(trimmed);
|
|
98
|
+
if (Number.isFinite(numeric))
|
|
99
|
+
return numeric > 1e11 ? Math.floor(numeric / 1000) : numeric;
|
|
100
|
+
const parsedMs = Date.parse(trimmed);
|
|
101
|
+
if (!Number.isFinite(parsedMs))
|
|
102
|
+
return null;
|
|
103
|
+
return Math.floor(parsedMs / 1000);
|
|
104
|
+
}
|
|
105
|
+
function unixSecondsToIso(value) {
|
|
106
|
+
const seconds = normalizeUnixSeconds(value);
|
|
107
|
+
if (seconds === null)
|
|
108
|
+
return null;
|
|
109
|
+
const date = new Date(seconds * 1000);
|
|
110
|
+
return Number.isFinite(date.getTime()) ? date.toISOString() : null;
|
|
111
|
+
}
|
|
112
|
+
// A category/topic enum becomes a site-supplied list; default to any string.
|
|
113
|
+
function slugEnum(values) {
|
|
114
|
+
return values && values.length > 0
|
|
115
|
+
? z.enum([values[0], ...values.slice(1)])
|
|
116
|
+
: z.string().min(1);
|
|
117
|
+
}
|
|
118
|
+
function buildSchemas(resolved) {
|
|
119
|
+
const category = slugEnum(resolved.primaryCategorySlugs);
|
|
120
|
+
const topic = slugEnum(resolved.primaryTopicSlugs);
|
|
121
|
+
const BaseSchema = z.object({
|
|
122
|
+
slug: z.string().min(3),
|
|
123
|
+
title: z.string().min(3),
|
|
124
|
+
seoTitle: z.string().optional().nullable(),
|
|
125
|
+
description: z.string().optional().nullable(),
|
|
126
|
+
excerpt: z.string().optional().nullable(),
|
|
127
|
+
byline: z.string().optional().nullable(),
|
|
128
|
+
primaryCategory: category.default(resolved.defaultPrimaryCategory),
|
|
129
|
+
primaryTopic: topic.default(resolved.defaultPrimaryTopic),
|
|
130
|
+
featured: z.boolean().optional(),
|
|
131
|
+
visibility: z.enum(['free', 'premium']).optional(),
|
|
132
|
+
authorId: z.string().optional().nullable(),
|
|
133
|
+
heroImageId: z.string().optional().nullable(),
|
|
134
|
+
heroImageAlt: z.string().optional().nullable(),
|
|
135
|
+
heroImageCaption: z.string().optional().nullable(),
|
|
136
|
+
socialImageId: z.string().optional().nullable(),
|
|
137
|
+
canonicalUrl: z.string().url().optional().nullable(),
|
|
138
|
+
tags: z.array(z.string()).optional(),
|
|
139
|
+
});
|
|
140
|
+
const CreateSchema = z.discriminatedUnion('type', [
|
|
141
|
+
BaseSchema.extend({
|
|
142
|
+
type: z.literal('article'),
|
|
143
|
+
content: z.object({
|
|
144
|
+
bodyMarkdown: z.string().min(1),
|
|
145
|
+
bodyHtml: z.string().optional().nullable(),
|
|
146
|
+
subtitle: z.string().optional().nullable(),
|
|
147
|
+
wordCount: z.number().int().optional().nullable(),
|
|
148
|
+
readTimeMinutes: z.number().int().optional().nullable(),
|
|
149
|
+
editorTakeaways: z.array(z.string().min(1)).max(4).optional().nullable(),
|
|
150
|
+
}),
|
|
151
|
+
}),
|
|
152
|
+
BaseSchema.extend({
|
|
153
|
+
type: z.literal('video'),
|
|
154
|
+
content: z.object({
|
|
155
|
+
script: z.string().default(''),
|
|
156
|
+
videoId: z.string().min(1),
|
|
157
|
+
durationSeconds: z.number().int().optional().nullable(),
|
|
158
|
+
thumbnailImageId: z.string().optional().nullable(),
|
|
159
|
+
processingSourceUrl: z.string().url().optional().nullable(),
|
|
160
|
+
processingSourceKind: z.string().min(1).optional().nullable(),
|
|
161
|
+
}),
|
|
162
|
+
}),
|
|
163
|
+
BaseSchema.extend({
|
|
164
|
+
type: z.literal('podcast'),
|
|
165
|
+
content: z.object({
|
|
166
|
+
// Empty allowed: a podcast is created as a draft with audio uploaded
|
|
167
|
+
// but no transcript yet — the transcription pipeline fills it async
|
|
168
|
+
// (Foundry callback hydrates podcast_content.transcript later). Mirrors
|
|
169
|
+
// the video `script` default-empty pattern above.
|
|
170
|
+
transcript: z.string().default(''),
|
|
171
|
+
audioR2Key: z.string().min(1),
|
|
172
|
+
durationSeconds: z.number().int().optional().nullable(),
|
|
173
|
+
}),
|
|
174
|
+
}),
|
|
175
|
+
BaseSchema.extend({
|
|
176
|
+
type: z.literal('newsletter'),
|
|
177
|
+
content: z.object({
|
|
178
|
+
bodyMarkdown: z.string().min(1),
|
|
179
|
+
bodyHtml: z.string().optional().nullable(),
|
|
180
|
+
subtitle: z.string().optional().nullable(),
|
|
181
|
+
wordCount: z.number().int().optional().nullable(),
|
|
182
|
+
readTimeMinutes: z.number().int().optional().nullable(),
|
|
183
|
+
editorTakeaways: z.array(z.string().min(1)).max(4).optional().nullable(),
|
|
184
|
+
}),
|
|
185
|
+
}),
|
|
186
|
+
]);
|
|
187
|
+
const RelationSchema = z.object({
|
|
188
|
+
relatedId: z.string().min(1),
|
|
189
|
+
rank: z.number().int().min(0),
|
|
190
|
+
reason: z.string().optional().nullable(),
|
|
191
|
+
});
|
|
192
|
+
const UpdateSchema = z.object({
|
|
193
|
+
slug: z.string().min(3).optional(),
|
|
194
|
+
title: z.string().min(3).optional(),
|
|
195
|
+
seoTitle: z.string().optional().nullable(),
|
|
196
|
+
description: z.string().optional().nullable(),
|
|
197
|
+
excerpt: z.string().optional().nullable(),
|
|
198
|
+
byline: z.string().optional().nullable(),
|
|
199
|
+
primaryCategory: category.optional().nullable(),
|
|
200
|
+
primaryTopic: topic.optional().nullable(),
|
|
201
|
+
featured: z.boolean().optional(),
|
|
202
|
+
visibility: z.enum(['free', 'premium']).optional(),
|
|
203
|
+
authorId: z.string().optional().nullable(),
|
|
204
|
+
heroImageId: z.string().optional().nullable(),
|
|
205
|
+
heroImageAlt: z.string().optional().nullable(),
|
|
206
|
+
heroImageCaption: z.string().optional().nullable(),
|
|
207
|
+
socialImageId: z.string().optional().nullable(),
|
|
208
|
+
canonicalUrl: z.string().url().optional().nullable(),
|
|
209
|
+
tags: z.array(z.string()).optional(),
|
|
210
|
+
content: z.record(z.string(), z.any()).optional(),
|
|
211
|
+
relations: z.array(RelationSchema).optional(),
|
|
212
|
+
aiLockedFields: z.array(z.string()).optional(),
|
|
213
|
+
/** SEO focus keyword (P3 SEO tab — migration 0004 column). */
|
|
214
|
+
seoFocusKeyword: z.string().optional().nullable(),
|
|
215
|
+
});
|
|
216
|
+
return { CreateSchema, UpdateSchema };
|
|
217
|
+
}
|
|
218
|
+
const ArticleContentUpdateSchema = z.object({
|
|
219
|
+
bodyMarkdown: z.string().optional(),
|
|
220
|
+
bodyHtml: z.string().optional().nullable(),
|
|
221
|
+
subtitle: z.string().optional().nullable(),
|
|
222
|
+
wordCount: z.number().int().optional().nullable(),
|
|
223
|
+
readTimeMinutes: z.number().int().optional().nullable(),
|
|
224
|
+
editorTakeaways: z.array(z.string().min(1)).max(4).optional().nullable(),
|
|
225
|
+
});
|
|
226
|
+
const VideoContentUpdateSchema = z.object({
|
|
227
|
+
script: z.string().optional(),
|
|
228
|
+
videoId: z.string().optional(),
|
|
229
|
+
durationSeconds: z.number().int().optional().nullable(),
|
|
230
|
+
thumbnailImageId: z.string().optional().nullable(),
|
|
231
|
+
processingSourceUrl: z.string().url().optional().nullable(),
|
|
232
|
+
processingSourceKind: z.string().min(1).optional().nullable(),
|
|
233
|
+
});
|
|
234
|
+
const PodcastContentUpdateSchema = z.object({
|
|
235
|
+
transcript: z.string().optional(),
|
|
236
|
+
audioR2Key: z.string().optional(),
|
|
237
|
+
durationSeconds: z.number().int().optional().nullable(),
|
|
238
|
+
});
|
|
239
|
+
function getContentUpdateSchema(type) {
|
|
240
|
+
switch (type) {
|
|
241
|
+
case 'article':
|
|
242
|
+
case 'newsletter':
|
|
243
|
+
return ArticleContentUpdateSchema;
|
|
244
|
+
case 'video':
|
|
245
|
+
return VideoContentUpdateSchema;
|
|
246
|
+
case 'podcast':
|
|
247
|
+
return PodcastContentUpdateSchema;
|
|
248
|
+
default:
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const ScheduleSchema = z
|
|
253
|
+
.object({
|
|
254
|
+
publishAt: z.string().min(1).optional().nullable(),
|
|
255
|
+
publishAtLocal: z.string().min(1).optional().nullable(),
|
|
256
|
+
timezone: z.string().optional().nullable(),
|
|
257
|
+
})
|
|
258
|
+
.refine((value) => Boolean(value.publishAt || value.publishAtLocal), {
|
|
259
|
+
message: 'publishAt or publishAtLocal is required',
|
|
260
|
+
path: ['publishAt'],
|
|
261
|
+
});
|
|
262
|
+
// Bulk operations schema — the Library bulk-action bar sends one of these.
|
|
263
|
+
// publish/schedule/unpublish use their own guarded action routes; bulk schedule
|
|
264
|
+
// is a convenience wrapper only (no body-validation gate — use the per-item
|
|
265
|
+
// action for pre-publish lint).
|
|
266
|
+
const BulkSchema = z.discriminatedUnion('op', [
|
|
267
|
+
z.object({ op: z.literal('trash'), ids: z.array(z.string().min(1)).min(1) }),
|
|
268
|
+
z.object({
|
|
269
|
+
op: z.literal('topic'),
|
|
270
|
+
ids: z.array(z.string().min(1)).min(1),
|
|
271
|
+
topic: z.string().min(1),
|
|
272
|
+
}),
|
|
273
|
+
z.object({
|
|
274
|
+
op: z.literal('schedule'),
|
|
275
|
+
ids: z.array(z.string().min(1)).min(1),
|
|
276
|
+
publishAt: z.string().min(1),
|
|
277
|
+
}),
|
|
278
|
+
]);
|
|
279
|
+
// set-status only allows the safe non-publish transitions.
|
|
280
|
+
// publish/schedule keep their dedicated guarded action routes.
|
|
281
|
+
const SetStatusSchema = z.object({
|
|
282
|
+
status: z.enum(['draft', 'review']),
|
|
283
|
+
});
|
|
284
|
+
// Contributors PUT body schema (P3 Task 3).
|
|
285
|
+
const ContributorItemSchema = z.object({
|
|
286
|
+
authorId: z.string().min(1),
|
|
287
|
+
role: z.string().optional().nullable(),
|
|
288
|
+
position: z.number().int().min(0).default(0),
|
|
289
|
+
});
|
|
290
|
+
const ContributorsReplaceSchema = z.object({
|
|
291
|
+
contributors: z.array(ContributorItemSchema),
|
|
292
|
+
});
|
|
293
|
+
function parseLocalDateTime(value) {
|
|
294
|
+
const match = LOCAL_DATE_TIME_RE.exec(value);
|
|
295
|
+
if (!match)
|
|
296
|
+
return null;
|
|
297
|
+
const [, y, m, d, h, min] = match;
|
|
298
|
+
const parts = {
|
|
299
|
+
year: Number.parseInt(y, 10),
|
|
300
|
+
month: Number.parseInt(m, 10),
|
|
301
|
+
day: Number.parseInt(d, 10),
|
|
302
|
+
hour: Number.parseInt(h, 10),
|
|
303
|
+
minute: Number.parseInt(min, 10),
|
|
304
|
+
};
|
|
305
|
+
if (!Number.isInteger(parts.year) ||
|
|
306
|
+
parts.month < 1 ||
|
|
307
|
+
parts.month > 12 ||
|
|
308
|
+
parts.day < 1 ||
|
|
309
|
+
parts.day > 31 ||
|
|
310
|
+
parts.hour < 0 ||
|
|
311
|
+
parts.hour > 23 ||
|
|
312
|
+
parts.minute < 0 ||
|
|
313
|
+
parts.minute > 59) {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
const dateCheck = new Date(Date.UTC(parts.year, parts.month - 1, parts.day));
|
|
317
|
+
if (dateCheck.getUTCFullYear() !== parts.year ||
|
|
318
|
+
dateCheck.getUTCMonth() + 1 !== parts.month ||
|
|
319
|
+
dateCheck.getUTCDate() !== parts.day) {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
return parts;
|
|
323
|
+
}
|
|
324
|
+
function getZonedParts(instantMs, timeZone) {
|
|
325
|
+
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
326
|
+
timeZone,
|
|
327
|
+
hour12: false,
|
|
328
|
+
year: 'numeric',
|
|
329
|
+
month: '2-digit',
|
|
330
|
+
day: '2-digit',
|
|
331
|
+
hour: '2-digit',
|
|
332
|
+
minute: '2-digit',
|
|
333
|
+
second: '2-digit',
|
|
334
|
+
});
|
|
335
|
+
const values = { year: '', month: '', day: '', hour: '', minute: '', second: '' };
|
|
336
|
+
for (const part of formatter.formatToParts(new Date(instantMs))) {
|
|
337
|
+
if (part.type in values)
|
|
338
|
+
values[part.type] = part.value;
|
|
339
|
+
}
|
|
340
|
+
const year = Number.parseInt(values.year, 10);
|
|
341
|
+
const month = Number.parseInt(values.month, 10);
|
|
342
|
+
const day = Number.parseInt(values.day, 10);
|
|
343
|
+
const hour = Number.parseInt(values.hour, 10);
|
|
344
|
+
const minute = Number.parseInt(values.minute, 10);
|
|
345
|
+
if (![year, month, day, hour, minute].every(Number.isFinite))
|
|
346
|
+
return null;
|
|
347
|
+
return { year, month, day, hour, minute };
|
|
348
|
+
}
|
|
349
|
+
function isValidTimeZone(timeZone) {
|
|
350
|
+
try {
|
|
351
|
+
new Intl.DateTimeFormat('en-US', { timeZone }).format(new Date());
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function getTimeZoneOffsetMs(instantMs, timeZone) {
|
|
359
|
+
const zoned = getZonedParts(instantMs, timeZone);
|
|
360
|
+
if (!zoned)
|
|
361
|
+
return null;
|
|
362
|
+
const asUtc = Date.UTC(zoned.year, zoned.month - 1, zoned.day, zoned.hour, zoned.minute, 0);
|
|
363
|
+
return asUtc - instantMs;
|
|
364
|
+
}
|
|
365
|
+
function parseLocalDateTimeInZoneToUnixSeconds(value, timeZone) {
|
|
366
|
+
const target = parseLocalDateTime(value);
|
|
367
|
+
if (!target)
|
|
368
|
+
return null;
|
|
369
|
+
const targetUtcMs = Date.UTC(target.year, target.month - 1, target.day, target.hour, target.minute, 0);
|
|
370
|
+
let candidateMs = targetUtcMs;
|
|
371
|
+
for (let i = 0; i < 5; i += 1) {
|
|
372
|
+
const offsetMs = getTimeZoneOffsetMs(candidateMs, timeZone);
|
|
373
|
+
if (offsetMs === null)
|
|
374
|
+
return null;
|
|
375
|
+
const next = targetUtcMs - offsetMs;
|
|
376
|
+
if (Math.abs(next - candidateMs) < 1000) {
|
|
377
|
+
candidateMs = next;
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
candidateMs = next;
|
|
381
|
+
}
|
|
382
|
+
const resolved = getZonedParts(candidateMs, timeZone);
|
|
383
|
+
if (!resolved ||
|
|
384
|
+
resolved.year !== target.year ||
|
|
385
|
+
resolved.month !== target.month ||
|
|
386
|
+
resolved.day !== target.day ||
|
|
387
|
+
resolved.hour !== target.hour ||
|
|
388
|
+
resolved.minute !== target.minute) {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
return Math.floor(candidateMs / 1000);
|
|
392
|
+
}
|
|
393
|
+
// --- read-side payload helpers (ported verbatim) --------------------------
|
|
394
|
+
async function getContentPayload(ctx, type, id) {
|
|
395
|
+
if (type === 'article' || type === 'newsletter') {
|
|
396
|
+
return ctx.db
|
|
397
|
+
.prepare(`SELECT body_markdown, body_html, word_count, read_time_minutes, subtitle,
|
|
398
|
+
ai_takeaways, ai_takeaways_at, ai_takeaways_model, editor_takeaways,
|
|
399
|
+
ai_takeaways_correlation_id
|
|
400
|
+
FROM article_content WHERE content_id = ? LIMIT 1`)
|
|
401
|
+
.bind(id)
|
|
402
|
+
.first();
|
|
403
|
+
}
|
|
404
|
+
if (type === 'video') {
|
|
405
|
+
return ctx.db
|
|
406
|
+
.prepare(`SELECT script, video_id, duration_seconds, thumbnail_image_id, processing_state,
|
|
407
|
+
processing_correlation_id, processing_started_at, processing_completed_at,
|
|
408
|
+
processing_last_event_at, processing_error, processing_source_url,
|
|
409
|
+
processing_source_kind, source_fetched, transcript_ready, hls_ready,
|
|
410
|
+
transcript_url, hls_manifest_url, hls_poster_url
|
|
411
|
+
FROM video_content WHERE content_id = ? LIMIT 1`)
|
|
412
|
+
.bind(id)
|
|
413
|
+
.first();
|
|
414
|
+
}
|
|
415
|
+
if (type === 'podcast') {
|
|
416
|
+
return ctx.db
|
|
417
|
+
.prepare('SELECT transcript, audio_r2_key, duration_seconds FROM podcast_content WHERE content_id = ? LIMIT 1')
|
|
418
|
+
.bind(id)
|
|
419
|
+
.first();
|
|
420
|
+
}
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
async function getContentTags(ctx, id) {
|
|
424
|
+
const result = await ctx.db
|
|
425
|
+
.prepare('SELECT t.slug FROM content_tag_links l JOIN content_tags t ON t.id = l.tag_id WHERE l.content_id = ? ORDER BY t.label ASC')
|
|
426
|
+
.bind(id)
|
|
427
|
+
.all();
|
|
428
|
+
return (result.results || []).map((row) => row.slug);
|
|
429
|
+
}
|
|
430
|
+
async function getMediaAssetUrls(db, ids) {
|
|
431
|
+
const uniqueIds = Array.from(new Set(ids.filter(Boolean)));
|
|
432
|
+
if (uniqueIds.length === 0)
|
|
433
|
+
return {};
|
|
434
|
+
const placeholders = uniqueIds.map(() => '?').join(',');
|
|
435
|
+
const result = await db
|
|
436
|
+
.prepare(`SELECT id, public_url, url FROM media_assets WHERE id IN (${placeholders})`)
|
|
437
|
+
.bind(...uniqueIds)
|
|
438
|
+
.all();
|
|
439
|
+
const urls = {};
|
|
440
|
+
for (const row of result.results ?? []) {
|
|
441
|
+
const url = row.public_url || row.url;
|
|
442
|
+
if (url)
|
|
443
|
+
urls[row.id] = url;
|
|
444
|
+
}
|
|
445
|
+
return urls;
|
|
446
|
+
}
|
|
447
|
+
async function readJson(ctx) {
|
|
448
|
+
return ctx.request.json().catch(() => null);
|
|
449
|
+
}
|
|
450
|
+
export function createContentRoutes(config) {
|
|
451
|
+
const resolved = resolveConfig(config);
|
|
452
|
+
const { CreateSchema, UpdateSchema } = buildSchemas(resolved);
|
|
453
|
+
const dispatchTakeaways = config.dispatchTakeaways;
|
|
454
|
+
const dispatchFaq = config.dispatchFaq;
|
|
455
|
+
const dispatchWebhook = config.dispatchWebhook;
|
|
456
|
+
const publishReadiness = config.publishReadiness;
|
|
457
|
+
const activeWorkspaceId = config.activeWorkspaceId ?? 'default';
|
|
458
|
+
async function maybeDispatchPodcastToFoundry(ctx, contentId) {
|
|
459
|
+
if (!resolved.hooks.foundryVideo)
|
|
460
|
+
return null;
|
|
461
|
+
const row = await ctx.db
|
|
462
|
+
.prepare(`SELECT transcript, audio_r2_key, processing_trigger_token
|
|
463
|
+
FROM podcast_content
|
|
464
|
+
WHERE content_id = ?
|
|
465
|
+
LIMIT 1`)
|
|
466
|
+
.bind(contentId)
|
|
467
|
+
.first();
|
|
468
|
+
const audioR2Key = row?.audio_r2_key?.trim() ?? '';
|
|
469
|
+
if (!audioR2Key)
|
|
470
|
+
return null;
|
|
471
|
+
if (row?.processing_trigger_token?.trim())
|
|
472
|
+
return null;
|
|
473
|
+
if (row?.transcript?.trim())
|
|
474
|
+
return null;
|
|
475
|
+
return dispatchToFoundry(ctx.db, contentId, resolved.hooks.foundryVideo);
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Fire the optional dispatchWebhook hook for a lifecycle transition.
|
|
479
|
+
*
|
|
480
|
+
* Wrapped in try/catch so a webhook dispatch failure NEVER fails the action
|
|
481
|
+
* — identical guard pattern to dispatchTakeaways / logActivity.
|
|
482
|
+
* When no hook is injected this is a no-op (no 501, no error).
|
|
483
|
+
*/
|
|
484
|
+
async function fireWebhook(ctx, eventType, data) {
|
|
485
|
+
if (!dispatchWebhook)
|
|
486
|
+
return;
|
|
487
|
+
try {
|
|
488
|
+
await dispatchWebhook(ctx, {
|
|
489
|
+
eventType,
|
|
490
|
+
workspaceId: activeWorkspaceId,
|
|
491
|
+
data,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
catch (err) {
|
|
495
|
+
console.error(`[cms] dispatchWebhook(${eventType}) failed`, err);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Capability gate for a content mutation. Enforces the 5-role matrix WITH the
|
|
500
|
+
* contributor/producer ownership predicate (canPerformContentAction).
|
|
501
|
+
*
|
|
502
|
+
* When the host wires the role seam (authz.getUser), this resolves the actor's
|
|
503
|
+
* role + subject and checks the action against the content's author. The launch
|
|
504
|
+
* roster (owner/senior_editor/editor) is NEVER ownership-restricted, so editors
|
|
505
|
+
* keep full editorial. contributor/producer are gated to their own/assigned
|
|
506
|
+
* content and blocked from publish/destructive actions.
|
|
507
|
+
*
|
|
508
|
+
* `contentAuthorId` is the content_items.author_id (the host maps an authored
|
|
509
|
+
* row to the actor's subject; the package compares the two for ownership).
|
|
510
|
+
*
|
|
511
|
+
* Backwards compatible: when authz.getUser is absent (host has not wired the
|
|
512
|
+
* role layer) this falls back to the flat requirePublisher gate.
|
|
513
|
+
*/
|
|
514
|
+
async function requireContentAction(ctx, action, contentAuthorId) {
|
|
515
|
+
// Role seam not wired → flat publisher gate (legacy hosts).
|
|
516
|
+
if (!resolved.authz.getUser) {
|
|
517
|
+
return resolved.authz.requirePublisher(ctx);
|
|
518
|
+
}
|
|
519
|
+
const user = await resolved.authz.getUser(ctx);
|
|
520
|
+
if (!user)
|
|
521
|
+
return json({ error: 'Unauthorized' }, 401);
|
|
522
|
+
const actorSubject = user.subject ?? ctx.userId ?? null;
|
|
523
|
+
const allowed = canPerformContentAction(user.role, { actorSubject: actorSubject ?? '', contentAuthorId }, action);
|
|
524
|
+
if (!allowed) {
|
|
525
|
+
return json({ error: 'Forbidden', action, role: user.role }, 403);
|
|
526
|
+
}
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
return {
|
|
530
|
+
async list(ctx) {
|
|
531
|
+
const denied = await resolved.authz.requirePublisher(ctx);
|
|
532
|
+
if (denied)
|
|
533
|
+
return denied;
|
|
534
|
+
const url = new URL(ctx.request.url);
|
|
535
|
+
const limit = Math.min(Number(url.searchParams.get('limit') || '20'), 50);
|
|
536
|
+
const status = parseStatus(url.searchParams.get('status'));
|
|
537
|
+
const types = parseTypes(url.searchParams.get('types'));
|
|
538
|
+
const cursor = parseCursor(url.searchParams.get('cursor'));
|
|
539
|
+
const q = url.searchParams.get('q') || null;
|
|
540
|
+
const author = url.searchParams.get('author') || null;
|
|
541
|
+
const bindings = [];
|
|
542
|
+
// Always exclude soft-deleted rows (migration 0002 added deleted_at).
|
|
543
|
+
let where = 'c.deleted_at IS NULL';
|
|
544
|
+
if (status) {
|
|
545
|
+
where += ' AND c.status = ?';
|
|
546
|
+
bindings.push(status);
|
|
547
|
+
}
|
|
548
|
+
if (types && types.length > 0) {
|
|
549
|
+
where += ` AND c.type IN (${types.map(() => '?').join(',')})`;
|
|
550
|
+
bindings.push(...types);
|
|
551
|
+
}
|
|
552
|
+
if (q) {
|
|
553
|
+
where += ' AND (c.title LIKE ? OR c.slug LIKE ?)';
|
|
554
|
+
const pattern = `%${q}%`;
|
|
555
|
+
bindings.push(pattern, pattern);
|
|
556
|
+
}
|
|
557
|
+
if (author) {
|
|
558
|
+
where += ' AND c.author_id = ?';
|
|
559
|
+
bindings.push(author);
|
|
560
|
+
}
|
|
561
|
+
if (cursor) {
|
|
562
|
+
where += ' AND (c.updated_at < ? OR (c.updated_at = ? AND c.id < ?))';
|
|
563
|
+
bindings.push(cursor.updatedAt, cursor.updatedAt, cursor.id);
|
|
564
|
+
}
|
|
565
|
+
bindings.push(limit + 1);
|
|
566
|
+
// LEFT JOIN video_content so each row carries its processing_state
|
|
567
|
+
// (null for non-video rows). The UI's StatusPill derives the
|
|
568
|
+
// "processing" pseudo-status from this; without it the field is
|
|
569
|
+
// undefined client-side and `processingState !== null` is true for
|
|
570
|
+
// EVERY row, mis-stamping all rows as PROCESSING. Mirrors the JOIN the
|
|
571
|
+
// counts handler below already uses.
|
|
572
|
+
const query = `
|
|
573
|
+
SELECT c.id, c.type, c.status, c.slug, c.title, c.visibility, c.featured,
|
|
574
|
+
c.publish_at, c.published_at, c.updated_at, c.author_id,
|
|
575
|
+
a.slug AS author_slug, a.name AS author_name,
|
|
576
|
+
vc.processing_state AS processing_state
|
|
577
|
+
FROM content_items c
|
|
578
|
+
LEFT JOIN authors a ON a.id = c.author_id
|
|
579
|
+
LEFT JOIN video_content vc ON vc.content_id = c.id
|
|
580
|
+
WHERE ${where}
|
|
581
|
+
ORDER BY c.updated_at DESC, c.id DESC
|
|
582
|
+
LIMIT ?`;
|
|
583
|
+
const result = await ctx.db
|
|
584
|
+
.prepare(query)
|
|
585
|
+
.bind(...bindings)
|
|
586
|
+
.all();
|
|
587
|
+
const rows = result.results || [];
|
|
588
|
+
const hasMore = rows.length > limit;
|
|
589
|
+
const sliced = hasMore ? rows.slice(0, limit) : rows;
|
|
590
|
+
const last = sliced[sliced.length - 1];
|
|
591
|
+
const lastUpdatedAt = last ? normalizeUnixSeconds(last.updated_at) : null;
|
|
592
|
+
const nextCursor = last && lastUpdatedAt !== null ? `${lastUpdatedAt}:${last.id}` : null;
|
|
593
|
+
const items = sliced.map((row) => ({
|
|
594
|
+
...row,
|
|
595
|
+
featured: Boolean(row.featured),
|
|
596
|
+
authorId: row.author_id ?? null,
|
|
597
|
+
authorSlug: row.author_slug ?? null,
|
|
598
|
+
authorName: row.author_name ?? null,
|
|
599
|
+
processingState: row.processing_state ?? null,
|
|
600
|
+
publishAt: unixSecondsToIso(row.publish_at),
|
|
601
|
+
publishedAt: unixSecondsToIso(row.published_at),
|
|
602
|
+
updatedAt: unixSecondsToIso(row.updated_at),
|
|
603
|
+
}));
|
|
604
|
+
return json({ items, nextCursor });
|
|
605
|
+
},
|
|
606
|
+
async counts(ctx) {
|
|
607
|
+
const denied = await resolved.authz.requirePublisher(ctx);
|
|
608
|
+
if (denied)
|
|
609
|
+
return denied;
|
|
610
|
+
// Count non-deleted items grouped by status.
|
|
611
|
+
// "processing" is a derived pseudo-status: video rows whose
|
|
612
|
+
// processing_state is NOT in ('ready','failed') — a mid-flight
|
|
613
|
+
// Foundry pipeline job. It is NOT a value in the content_items.status
|
|
614
|
+
// CHECK enum (draft|scheduled|review|published|archived).
|
|
615
|
+
const rows = await ctx.db
|
|
616
|
+
.prepare(`SELECT
|
|
617
|
+
COUNT(*) AS total,
|
|
618
|
+
SUM(CASE WHEN c.status = 'draft' THEN 1 ELSE 0 END) AS draft,
|
|
619
|
+
SUM(CASE WHEN c.status = 'scheduled' THEN 1 ELSE 0 END) AS scheduled,
|
|
620
|
+
SUM(CASE WHEN c.status = 'review' THEN 1 ELSE 0 END) AS review,
|
|
621
|
+
SUM(CASE WHEN c.status = 'published' THEN 1 ELSE 0 END) AS published,
|
|
622
|
+
SUM(CASE WHEN vc.processing_state IS NOT NULL
|
|
623
|
+
AND vc.processing_state NOT IN ('ready','failed')
|
|
624
|
+
THEN 1 ELSE 0 END) AS processing
|
|
625
|
+
FROM content_items c
|
|
626
|
+
LEFT JOIN video_content vc ON vc.content_id = c.id
|
|
627
|
+
WHERE c.deleted_at IS NULL`)
|
|
628
|
+
.first();
|
|
629
|
+
return json({
|
|
630
|
+
counts: {
|
|
631
|
+
all: rows?.total ?? 0,
|
|
632
|
+
draft: rows?.draft ?? 0,
|
|
633
|
+
scheduled: rows?.scheduled ?? 0,
|
|
634
|
+
review: rows?.review ?? 0,
|
|
635
|
+
published: rows?.published ?? 0,
|
|
636
|
+
processing: rows?.processing ?? 0,
|
|
637
|
+
},
|
|
638
|
+
});
|
|
639
|
+
},
|
|
640
|
+
async bulk(ctx) {
|
|
641
|
+
const parsed = BulkSchema.safeParse(await readJson(ctx));
|
|
642
|
+
if (!parsed.success) {
|
|
643
|
+
return json({ error: 'Invalid request', details: parsed.error.flatten() }, 400);
|
|
644
|
+
}
|
|
645
|
+
const { op, ids } = parsed.data;
|
|
646
|
+
// Bulk ops act across many (cross-author) items → require publish-level
|
|
647
|
+
// authority (publish_others_draft). contentAuthorId null forces the
|
|
648
|
+
// stricter bar, blocking contributor/producer; editors+ pass.
|
|
649
|
+
const bulkAction = op === 'trash' ? 'trash' : op === 'schedule' ? 'schedule' : 'publish';
|
|
650
|
+
const denied = await requireContentAction(ctx, bulkAction, null);
|
|
651
|
+
if (denied)
|
|
652
|
+
return denied;
|
|
653
|
+
if (op === 'trash') {
|
|
654
|
+
for (const id of ids) {
|
|
655
|
+
await softDeleteContent(ctx.db, id);
|
|
656
|
+
}
|
|
657
|
+
return json({ trashed: ids });
|
|
658
|
+
}
|
|
659
|
+
if (op === 'topic') {
|
|
660
|
+
const { topic } = parsed.data;
|
|
661
|
+
for (const id of ids) {
|
|
662
|
+
await ctx.db
|
|
663
|
+
.prepare('UPDATE content_items SET primary_topic = ?, updated_at = unixepoch() WHERE id = ? AND deleted_at IS NULL')
|
|
664
|
+
.bind(topic, id)
|
|
665
|
+
.run();
|
|
666
|
+
}
|
|
667
|
+
return json({ updated: ids });
|
|
668
|
+
}
|
|
669
|
+
if (op === 'schedule') {
|
|
670
|
+
const { publishAt } = parsed.data;
|
|
671
|
+
const publishAtMs = Date.parse(publishAt);
|
|
672
|
+
if (!Number.isFinite(publishAtMs)) {
|
|
673
|
+
return json({ error: 'Invalid publishAt' }, 400);
|
|
674
|
+
}
|
|
675
|
+
const publishAtSeconds = Math.floor(publishAtMs / 1000);
|
|
676
|
+
for (const id of ids) {
|
|
677
|
+
await scheduleContent(ctx.db, id, publishAtSeconds);
|
|
678
|
+
}
|
|
679
|
+
return json({ scheduled: ids });
|
|
680
|
+
}
|
|
681
|
+
// Exhaustive – the discriminated union above covers all ops.
|
|
682
|
+
return json({ error: 'Unhandled op' }, 400);
|
|
683
|
+
},
|
|
684
|
+
async create(ctx) {
|
|
685
|
+
// Create = write a new draft the actor authors. Pass the actor's own
|
|
686
|
+
// subject as the content author so the ownership predicate treats it as
|
|
687
|
+
// own content (contributor/producer may create their own drafts).
|
|
688
|
+
const actor = resolved.authz.getUser ? await resolved.authz.getUser(ctx) : null;
|
|
689
|
+
const ownAuthor = actor?.subject ?? ctx.userId ?? null;
|
|
690
|
+
const denied = await requireContentAction(ctx, 'write', ownAuthor);
|
|
691
|
+
if (denied)
|
|
692
|
+
return denied;
|
|
693
|
+
const parsed = CreateSchema.safeParse(await readJson(ctx));
|
|
694
|
+
if (!parsed.success) {
|
|
695
|
+
return json({ error: 'Invalid request', details: parsed.error.flatten() }, 400);
|
|
696
|
+
}
|
|
697
|
+
const created = await createContent(ctx.db, parsed.data);
|
|
698
|
+
const podcastDispatch = parsed.data.type === 'podcast' ? await maybeDispatchPodcastToFoundry(ctx, created.id) : null;
|
|
699
|
+
// Fire content.created webhook (lifecycle transition).
|
|
700
|
+
await fireWebhook(ctx, 'content.created', {
|
|
701
|
+
id: created.id,
|
|
702
|
+
slug: created.slug,
|
|
703
|
+
type: parsed.data.type,
|
|
704
|
+
});
|
|
705
|
+
return json({
|
|
706
|
+
id: created.id,
|
|
707
|
+
slug: created.slug,
|
|
708
|
+
podcastFoundryQueued: Boolean(podcastDispatch),
|
|
709
|
+
podcastFoundryCorrelationId: podcastDispatch?.correlationId ?? null,
|
|
710
|
+
}, 201);
|
|
711
|
+
},
|
|
712
|
+
async get(ctx) {
|
|
713
|
+
const denied = await resolved.authz.requirePublisher(ctx);
|
|
714
|
+
if (denied)
|
|
715
|
+
return denied;
|
|
716
|
+
const idResult = requireId(ctx);
|
|
717
|
+
if ('error' in idResult)
|
|
718
|
+
return idResult.error;
|
|
719
|
+
const id = idResult.id;
|
|
720
|
+
const item = await getContentItem(ctx.db, id);
|
|
721
|
+
if (!item)
|
|
722
|
+
return json({ error: 'Not found' }, 404);
|
|
723
|
+
const [content, tags, relations, author] = await Promise.all([
|
|
724
|
+
getContentPayload(ctx, item.type, id),
|
|
725
|
+
getContentTags(ctx, id),
|
|
726
|
+
getContentRelations(ctx.db, id),
|
|
727
|
+
item.author_id
|
|
728
|
+
? ctx.db
|
|
729
|
+
.prepare('SELECT name, slug FROM authors WHERE id = ? LIMIT 1')
|
|
730
|
+
.bind(item.author_id)
|
|
731
|
+
.first()
|
|
732
|
+
: Promise.resolve(null),
|
|
733
|
+
]);
|
|
734
|
+
const contentRecord = content;
|
|
735
|
+
const thumbnailImageId = typeof contentRecord?.thumbnail_image_id === 'string'
|
|
736
|
+
? contentRecord.thumbnail_image_id
|
|
737
|
+
: null;
|
|
738
|
+
const mediaUrls = await getMediaAssetUrls(ctx.db, [item.hero_image_id, thumbnailImageId].filter((value) => Boolean(value)));
|
|
739
|
+
const heroImageUrl = item.hero_image_id ? (mediaUrls[item.hero_image_id] ?? null) : null;
|
|
740
|
+
const thumbnailImageUrl = thumbnailImageId ? (mediaUrls[thumbnailImageId] ?? null) : null;
|
|
741
|
+
const itemWithMedia = {
|
|
742
|
+
...item,
|
|
743
|
+
hero_image_url: heroImageUrl,
|
|
744
|
+
author_name: author?.name ?? null,
|
|
745
|
+
author_slug: author?.slug ?? null,
|
|
746
|
+
};
|
|
747
|
+
const contentWithMedia = contentRecord
|
|
748
|
+
? {
|
|
749
|
+
...contentRecord,
|
|
750
|
+
...(thumbnailImageId ? { thumbnail_image_url: thumbnailImageUrl } : {}),
|
|
751
|
+
}
|
|
752
|
+
: content;
|
|
753
|
+
const aiLockedFields = (() => {
|
|
754
|
+
try {
|
|
755
|
+
return JSON.parse(item.ai_locked_fields || '[]');
|
|
756
|
+
}
|
|
757
|
+
catch {
|
|
758
|
+
return [];
|
|
759
|
+
}
|
|
760
|
+
})();
|
|
761
|
+
return json({
|
|
762
|
+
item: itemWithMedia,
|
|
763
|
+
content: contentWithMedia,
|
|
764
|
+
tags,
|
|
765
|
+
relations,
|
|
766
|
+
aiLockedFields,
|
|
767
|
+
});
|
|
768
|
+
},
|
|
769
|
+
async update(ctx) {
|
|
770
|
+
const idResult = requireId(ctx);
|
|
771
|
+
if ('error' in idResult)
|
|
772
|
+
return idResult.error;
|
|
773
|
+
const id = idResult.id;
|
|
774
|
+
const existing = await getContentItem(ctx.db, id);
|
|
775
|
+
if (!existing)
|
|
776
|
+
return json({ error: 'Not found' }, 404);
|
|
777
|
+
// Edit = write; contributor/producer gated to their own content.
|
|
778
|
+
const denied = await requireContentAction(ctx, 'write', existing.author_id);
|
|
779
|
+
if (denied)
|
|
780
|
+
return denied;
|
|
781
|
+
const parsed = UpdateSchema.safeParse(await readJson(ctx));
|
|
782
|
+
if (!parsed.success) {
|
|
783
|
+
return json({ error: 'Invalid request', details: parsed.error.flatten() }, 400);
|
|
784
|
+
}
|
|
785
|
+
if (parsed.data.content !== undefined) {
|
|
786
|
+
const contentSchema = getContentUpdateSchema(existing.type);
|
|
787
|
+
if (!contentSchema)
|
|
788
|
+
return json({ error: 'Invalid content type' }, 400);
|
|
789
|
+
const contentParsed = contentSchema.safeParse(parsed.data.content);
|
|
790
|
+
if (!contentParsed.success) {
|
|
791
|
+
return json({ error: 'Invalid request', details: contentParsed.error.flatten() }, 400);
|
|
792
|
+
}
|
|
793
|
+
parsed.data.content = contentParsed.data;
|
|
794
|
+
// Pre-publish content lint: refuse to overwrite an article body with
|
|
795
|
+
// content that contains known formatting artifacts. Draft saves run
|
|
796
|
+
// the same check but allow empty bodies (the auto-save path needs to
|
|
797
|
+
// persist work in progress); publish-time validation rejects empty
|
|
798
|
+
// bodies separately.
|
|
799
|
+
if (existing.type === 'article' || existing.type === 'newsletter') {
|
|
800
|
+
const articleContent = contentParsed.data;
|
|
801
|
+
const bodyMarkdown = articleContent.bodyMarkdown;
|
|
802
|
+
const bodyHtml = articleContent.bodyHtml;
|
|
803
|
+
const isDraft = existing.status === 'draft';
|
|
804
|
+
if (bodyMarkdown !== undefined || bodyHtml !== undefined) {
|
|
805
|
+
const guard = evaluateArticleBody({ body_markdown: bodyMarkdown ?? null, body_html: bodyHtml ?? null }, { source: 'cms.patch', contentId: id, requireBody: !isDraft });
|
|
806
|
+
if (!guard.ok) {
|
|
807
|
+
return json({ error: 'Article body failed pre-publish validation', errors: guard.errors }, 422);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
const priorSlug = existing.slug;
|
|
813
|
+
const updated = await updateContentItem(ctx.db, id, parsed.data);
|
|
814
|
+
if (!updated)
|
|
815
|
+
return json({ error: 'Not found' }, 404);
|
|
816
|
+
const podcastDispatch = existing.type === 'podcast' ? await maybeDispatchPodcastToFoundry(ctx, id) : null;
|
|
817
|
+
// If the slug changed, record the old slug as a redirect (spec §8).
|
|
818
|
+
if (updated.slug !== priorSlug) {
|
|
819
|
+
await recordSlugRedirect(ctx.db, priorSlug, id);
|
|
820
|
+
}
|
|
821
|
+
return json({
|
|
822
|
+
id,
|
|
823
|
+
slug: updated.slug,
|
|
824
|
+
podcastFoundryQueued: Boolean(podcastDispatch),
|
|
825
|
+
podcastFoundryCorrelationId: podcastDispatch?.correlationId ?? null,
|
|
826
|
+
});
|
|
827
|
+
},
|
|
828
|
+
async action(ctx) {
|
|
829
|
+
const idResult = requireId(ctx);
|
|
830
|
+
if ('error' in idResult)
|
|
831
|
+
return idResult.error;
|
|
832
|
+
const id = idResult.id;
|
|
833
|
+
const url = new URL(ctx.request.url);
|
|
834
|
+
const action = url.searchParams.get('action');
|
|
835
|
+
const existing = await getContentItem(ctx.db, id);
|
|
836
|
+
if (!existing)
|
|
837
|
+
return json({ error: 'Not found' }, 404);
|
|
838
|
+
// Map the action verb to a capability, then gate with the ownership
|
|
839
|
+
// predicate (contributor/producer scoped; owner/senior_editor/editor full).
|
|
840
|
+
const contentAction = mapActionToCapability(action, existing.type);
|
|
841
|
+
const denied = await requireContentAction(ctx, contentAction, existing.author_id);
|
|
842
|
+
if (denied)
|
|
843
|
+
return denied;
|
|
844
|
+
if (action === 'schedule') {
|
|
845
|
+
const parsed = ScheduleSchema.safeParse(await readJson(ctx));
|
|
846
|
+
if (!parsed.success) {
|
|
847
|
+
return json({ error: 'Invalid request', details: parsed.error.flatten() }, 400);
|
|
848
|
+
}
|
|
849
|
+
let publishAtSeconds = null;
|
|
850
|
+
const publishTimeZone = parsed.data.timezone || resolved.publishTimezone || PUBLISH_TIMEZONE_FALLBACK;
|
|
851
|
+
if (!isValidTimeZone(publishTimeZone)) {
|
|
852
|
+
return json({ error: 'Invalid timezone' }, 400);
|
|
853
|
+
}
|
|
854
|
+
if (parsed.data.publishAtLocal) {
|
|
855
|
+
publishAtSeconds = parseLocalDateTimeInZoneToUnixSeconds(parsed.data.publishAtLocal, publishTimeZone);
|
|
856
|
+
if (!Number.isFinite(publishAtSeconds)) {
|
|
857
|
+
return json({ error: `Invalid publishAtLocal for ${publishTimeZone} timezone` }, 400);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
else if (parsed.data.publishAt) {
|
|
861
|
+
const publishAt = Date.parse(parsed.data.publishAt);
|
|
862
|
+
if (!Number.isFinite(publishAt))
|
|
863
|
+
return json({ error: 'Invalid publishAt' }, 400);
|
|
864
|
+
publishAtSeconds = Math.floor(publishAt / 1000);
|
|
865
|
+
}
|
|
866
|
+
if (!Number.isFinite(publishAtSeconds)) {
|
|
867
|
+
return json({ error: 'Missing publish time' }, 400);
|
|
868
|
+
}
|
|
869
|
+
await scheduleContent(ctx.db, id, publishAtSeconds, publishTimeZone);
|
|
870
|
+
// Fire content.scheduled webhook.
|
|
871
|
+
await fireWebhook(ctx, 'content.scheduled', {
|
|
872
|
+
id,
|
|
873
|
+
type: existing.type,
|
|
874
|
+
title: existing.title,
|
|
875
|
+
publishAt: publishAtSeconds,
|
|
876
|
+
});
|
|
877
|
+
return json({ status: 'scheduled' });
|
|
878
|
+
}
|
|
879
|
+
if (action === 'publish') {
|
|
880
|
+
const now = Math.floor(Date.now() / 1000);
|
|
881
|
+
const wasDraft = existing.status === 'draft';
|
|
882
|
+
let podcastDispatch = null;
|
|
883
|
+
if (existing.type === 'podcast') {
|
|
884
|
+
try {
|
|
885
|
+
podcastDispatch = await maybeDispatchPodcastToFoundry(ctx, id);
|
|
886
|
+
}
|
|
887
|
+
catch (error) {
|
|
888
|
+
console.error('Podcast Foundry dispatch failed before publish', error);
|
|
889
|
+
return json({ error: 'Foundry dispatch failed' }, 502);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
if (publishReadiness) {
|
|
893
|
+
let readiness;
|
|
894
|
+
try {
|
|
895
|
+
readiness = await publishReadiness({
|
|
896
|
+
ctx,
|
|
897
|
+
contentId: id,
|
|
898
|
+
type: existing.type,
|
|
899
|
+
status: existing.status,
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
catch (err) {
|
|
903
|
+
console.error('[cms] publishReadiness failed', err);
|
|
904
|
+
return json({ error: 'Publish readiness check failed' }, 503);
|
|
905
|
+
}
|
|
906
|
+
if (!readiness.ready) {
|
|
907
|
+
return json({
|
|
908
|
+
error: 'Content is not ready to publish',
|
|
909
|
+
state: readiness.state,
|
|
910
|
+
blockers: readiness.blockers ?? [],
|
|
911
|
+
message: readiness.message,
|
|
912
|
+
etaLabel: readiness.etaLabel,
|
|
913
|
+
checkedAt: readiness.checkedAt,
|
|
914
|
+
}, readiness.status ?? 409);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
const guard = await evaluateContentBodyForPublish(ctx.db, id, 'cms.publish');
|
|
918
|
+
if (guard && !guard.ok) {
|
|
919
|
+
return json({
|
|
920
|
+
error: 'Article body failed pre-publish validation',
|
|
921
|
+
source: 'cms.publish',
|
|
922
|
+
errors: guard.errors,
|
|
923
|
+
}, 422);
|
|
924
|
+
}
|
|
925
|
+
await publishContent(ctx.db, id, now);
|
|
926
|
+
await createRevision(ctx.db, id, ctx.userId || null);
|
|
927
|
+
// Write first-party activity log row (cms_activity_log, P5 Task 3).
|
|
928
|
+
// Wrapped so a logging failure never breaks the publish response.
|
|
929
|
+
try {
|
|
930
|
+
await logActivity(ctx.db, {
|
|
931
|
+
workspaceId: activeWorkspaceId,
|
|
932
|
+
userId: ctx.userId ?? null,
|
|
933
|
+
action: 'published',
|
|
934
|
+
targetId: id,
|
|
935
|
+
targetTitle: existing.title,
|
|
936
|
+
type: existing.type,
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
catch (err) {
|
|
940
|
+
console.error('logActivity failed after publish', err);
|
|
941
|
+
}
|
|
942
|
+
let takeawaysQueued = false;
|
|
943
|
+
if (wasDraft && existing.type === 'article' && dispatchTakeaways) {
|
|
944
|
+
try {
|
|
945
|
+
const result = await dispatchTakeaways({ ctx, contentId: id, force: false });
|
|
946
|
+
takeawaysQueued = result.queued;
|
|
947
|
+
}
|
|
948
|
+
catch (error) {
|
|
949
|
+
console.error('Article takeaway dispatch failed', error);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
let faqQueued = false;
|
|
953
|
+
if (wasDraft && existing.type === 'article' && dispatchFaq) {
|
|
954
|
+
try {
|
|
955
|
+
const result = await dispatchFaq({ ctx, contentId: id, force: false });
|
|
956
|
+
faqQueued = result.queued;
|
|
957
|
+
}
|
|
958
|
+
catch (error) {
|
|
959
|
+
console.error('Article FAQ dispatch failed', error);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
// Fire content.published webhook.
|
|
963
|
+
await fireWebhook(ctx, 'content.published', {
|
|
964
|
+
id,
|
|
965
|
+
type: existing.type,
|
|
966
|
+
title: existing.title,
|
|
967
|
+
});
|
|
968
|
+
return json({
|
|
969
|
+
status: 'published',
|
|
970
|
+
takeawaysQueued,
|
|
971
|
+
faqQueued,
|
|
972
|
+
podcastFoundryQueued: Boolean(podcastDispatch),
|
|
973
|
+
podcastFoundryCorrelationId: podcastDispatch?.correlationId ?? null,
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
if (action === 'regenerate-takeaways') {
|
|
977
|
+
if (existing.type !== 'article')
|
|
978
|
+
return json({ error: 'Invalid content type' }, 400);
|
|
979
|
+
if (!dispatchTakeaways) {
|
|
980
|
+
return json({ error: 'Takeaways generation is not configured' }, 501);
|
|
981
|
+
}
|
|
982
|
+
try {
|
|
983
|
+
const result = await dispatchTakeaways({ ctx, contentId: id, force: true });
|
|
984
|
+
if (!result.queued) {
|
|
985
|
+
return json({ status: 'skipped', reason: result.reason || 'not queued' }, 409);
|
|
986
|
+
}
|
|
987
|
+
return json({ status: 'queued', correlationId: result.correlationId ?? null }, 202);
|
|
988
|
+
}
|
|
989
|
+
catch (error) {
|
|
990
|
+
console.error('Article takeaway regeneration failed', error);
|
|
991
|
+
return json({ error: 'Foundry dispatch failed' }, 502);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
if (action === 'regenerate-faq') {
|
|
995
|
+
if (existing.type !== 'article')
|
|
996
|
+
return json({ error: 'Invalid content type' }, 400);
|
|
997
|
+
if (!dispatchFaq) {
|
|
998
|
+
return json({ error: 'FAQ generation is not configured' }, 501);
|
|
999
|
+
}
|
|
1000
|
+
try {
|
|
1001
|
+
const result = await dispatchFaq({ ctx, contentId: id, force: true });
|
|
1002
|
+
if (!result.queued) {
|
|
1003
|
+
return json({ status: 'skipped', reason: result.reason || 'not queued' }, 409);
|
|
1004
|
+
}
|
|
1005
|
+
return json({ status: 'queued', correlationId: result.correlationId ?? null }, 202);
|
|
1006
|
+
}
|
|
1007
|
+
catch (error) {
|
|
1008
|
+
console.error('Article FAQ regeneration failed', error);
|
|
1009
|
+
return json({ error: 'FAQ dispatch failed' }, 502);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
if (action === 'unpublish') {
|
|
1013
|
+
await unpublishContent(ctx.db, id);
|
|
1014
|
+
// Fire content.unpublished webhook.
|
|
1015
|
+
await fireWebhook(ctx, 'content.unpublished', {
|
|
1016
|
+
id,
|
|
1017
|
+
type: existing.type,
|
|
1018
|
+
title: existing.title,
|
|
1019
|
+
});
|
|
1020
|
+
return json({ status: 'draft' });
|
|
1021
|
+
}
|
|
1022
|
+
if (action === 'archive') {
|
|
1023
|
+
await ctx.db
|
|
1024
|
+
.prepare("UPDATE content_items SET status = 'archived', updated_at = unixepoch() WHERE id = ?")
|
|
1025
|
+
.bind(id)
|
|
1026
|
+
.run();
|
|
1027
|
+
// Fire content.deleted (archive = soft delete lifecycle) webhook.
|
|
1028
|
+
await fireWebhook(ctx, 'content.deleted', {
|
|
1029
|
+
id,
|
|
1030
|
+
type: existing.type,
|
|
1031
|
+
title: existing.title,
|
|
1032
|
+
reason: 'archived',
|
|
1033
|
+
});
|
|
1034
|
+
return json({ status: 'archived' });
|
|
1035
|
+
}
|
|
1036
|
+
if (action === 'unschedule') {
|
|
1037
|
+
// publish_tz is NOT NULL (DEFAULT 'Europe/Paris'); reset it to the
|
|
1038
|
+
// default rather than nulling it (a NULL violates the constraint).
|
|
1039
|
+
await ctx.db
|
|
1040
|
+
.prepare("UPDATE content_items SET status = 'draft', publish_at = NULL, publish_tz = 'Europe/Paris', updated_at = unixepoch() WHERE id = ?")
|
|
1041
|
+
.bind(id)
|
|
1042
|
+
.run();
|
|
1043
|
+
return json({ status: 'draft' });
|
|
1044
|
+
}
|
|
1045
|
+
if (action === 'duplicate') {
|
|
1046
|
+
const result = await duplicateContentItem(ctx.db, id);
|
|
1047
|
+
if (!result)
|
|
1048
|
+
return json({ error: 'Not found' }, 404);
|
|
1049
|
+
// Fire content.duplicated webhook.
|
|
1050
|
+
await fireWebhook(ctx, 'content.duplicated', {
|
|
1051
|
+
id: result.id,
|
|
1052
|
+
slug: result.slug,
|
|
1053
|
+
sourceId: id,
|
|
1054
|
+
type: existing.type,
|
|
1055
|
+
});
|
|
1056
|
+
return json({ id: result.id, slug: result.slug }, 201);
|
|
1057
|
+
}
|
|
1058
|
+
if (action === 'set-status') {
|
|
1059
|
+
const parsed = SetStatusSchema.safeParse(await readJson(ctx));
|
|
1060
|
+
if (!parsed.success) {
|
|
1061
|
+
return json({
|
|
1062
|
+
error: 'Invalid request — status must be "draft" or "review"',
|
|
1063
|
+
details: parsed.error.flatten(),
|
|
1064
|
+
}, 400);
|
|
1065
|
+
}
|
|
1066
|
+
const { status } = parsed.data;
|
|
1067
|
+
// set-status only ever moves to draft/review (never scheduled), so it is
|
|
1068
|
+
// always a transition OUT of any scheduled state. Clear the stale
|
|
1069
|
+
// publish_at so a de-scheduled item doesn't carry a phantom publish time.
|
|
1070
|
+
// publish_tz is NOT NULL (DEFAULT 'Europe/Paris'), so it is reset to the
|
|
1071
|
+
// default rather than nulled.
|
|
1072
|
+
await ctx.db
|
|
1073
|
+
.prepare("UPDATE content_items SET status = ?, publish_at = NULL, publish_tz = 'Europe/Paris', updated_at = unixepoch() WHERE id = ?")
|
|
1074
|
+
.bind(status, id)
|
|
1075
|
+
.run();
|
|
1076
|
+
// Fire content.review when submitted for review.
|
|
1077
|
+
if (status === 'review') {
|
|
1078
|
+
await fireWebhook(ctx, 'content.review', {
|
|
1079
|
+
id,
|
|
1080
|
+
type: existing.type,
|
|
1081
|
+
title: existing.title,
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
return json({ status });
|
|
1085
|
+
}
|
|
1086
|
+
if (action === 'snapshot') {
|
|
1087
|
+
const revision = await createRevisionWithDelta(ctx.db, id, {
|
|
1088
|
+
createdBy: ctx.userId ?? null,
|
|
1089
|
+
tag: 'autosave',
|
|
1090
|
+
});
|
|
1091
|
+
if (!revision)
|
|
1092
|
+
return json({ error: 'Not found' }, 404);
|
|
1093
|
+
return json({ status: 'snapshot', revisionId: revision.id });
|
|
1094
|
+
}
|
|
1095
|
+
if (action === 'dispatch-to-foundry') {
|
|
1096
|
+
// Media-type guard: only video and podcast can be dispatched.
|
|
1097
|
+
if (existing.type !== 'video' && existing.type !== 'podcast') {
|
|
1098
|
+
return json({
|
|
1099
|
+
error: 'dispatch-to-foundry is only valid for video and podcast content types',
|
|
1100
|
+
}, 400);
|
|
1101
|
+
}
|
|
1102
|
+
// Hook-presence guard (mirrors the dispatchTakeaways 501 pattern).
|
|
1103
|
+
if (!resolved.hooks.foundryVideo) {
|
|
1104
|
+
return json({ error: 'Foundry video dispatch is not configured' }, 501);
|
|
1105
|
+
}
|
|
1106
|
+
if (existing.type === 'podcast') {
|
|
1107
|
+
const row = await ctx.db
|
|
1108
|
+
.prepare(`SELECT processing_trigger_token
|
|
1109
|
+
FROM podcast_content
|
|
1110
|
+
WHERE content_id = ?
|
|
1111
|
+
LIMIT 1`)
|
|
1112
|
+
.bind(id)
|
|
1113
|
+
.first();
|
|
1114
|
+
const existingToken = row?.processing_trigger_token?.trim() ?? '';
|
|
1115
|
+
if (existingToken) {
|
|
1116
|
+
return json({
|
|
1117
|
+
status: 'queued',
|
|
1118
|
+
correlationId: null,
|
|
1119
|
+
triggerToken: existingToken,
|
|
1120
|
+
skipped: true,
|
|
1121
|
+
}, 202);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
try {
|
|
1125
|
+
const result = await dispatchToFoundry(ctx.db, id, resolved.hooks.foundryVideo);
|
|
1126
|
+
return json({
|
|
1127
|
+
status: 'queued',
|
|
1128
|
+
correlationId: result.correlationId,
|
|
1129
|
+
triggerToken: result.triggerToken,
|
|
1130
|
+
}, 202);
|
|
1131
|
+
}
|
|
1132
|
+
catch (error) {
|
|
1133
|
+
console.error('Foundry dispatch failed', error);
|
|
1134
|
+
return json({ error: 'Foundry dispatch failed' }, 502);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
if (action === 'trash') {
|
|
1138
|
+
await softDeleteContent(ctx.db, id);
|
|
1139
|
+
// Fire content.deleted (trash) webhook.
|
|
1140
|
+
await fireWebhook(ctx, 'content.deleted', {
|
|
1141
|
+
id,
|
|
1142
|
+
type: existing.type,
|
|
1143
|
+
title: existing.title,
|
|
1144
|
+
reason: 'trashed',
|
|
1145
|
+
});
|
|
1146
|
+
return json({ status: 'trashed' });
|
|
1147
|
+
}
|
|
1148
|
+
return json({ error: 'Unknown action' }, 400);
|
|
1149
|
+
},
|
|
1150
|
+
async foundryCallback(ctx) {
|
|
1151
|
+
// HMAC-guarded machine callback — NOT behind requirePublisher.
|
|
1152
|
+
// The HMAC secret is read from ctx.env (Secrets Store binding on the
|
|
1153
|
+
// Workers side; `env.FOUNDRY_HMAC_SECRET`). Never a value in code/D1.
|
|
1154
|
+
const hmacSecret = typeof ctx.env?.FOUNDRY_HMAC_SECRET === 'string' ? ctx.env.FOUNDRY_HMAC_SECRET : null;
|
|
1155
|
+
if (!hmacSecret) {
|
|
1156
|
+
// No secret configured — refuse with 500 (misconfiguration).
|
|
1157
|
+
return json({ error: 'FOUNDRY_HMAC_SECRET is not configured' }, 500);
|
|
1158
|
+
}
|
|
1159
|
+
// Read the raw body text (needed for HMAC verification over the exact bytes).
|
|
1160
|
+
let rawBody;
|
|
1161
|
+
try {
|
|
1162
|
+
rawBody = await ctx.request.text();
|
|
1163
|
+
}
|
|
1164
|
+
catch {
|
|
1165
|
+
return json({ error: 'Failed to read request body' }, 400);
|
|
1166
|
+
}
|
|
1167
|
+
const signature = ctx.request.headers.get('X-Foundry-Signature') ?? '';
|
|
1168
|
+
const valid = await verifyHmac(hmacSecret, rawBody, signature);
|
|
1169
|
+
if (!valid) {
|
|
1170
|
+
return json({ error: 'Invalid signature' }, 401);
|
|
1171
|
+
}
|
|
1172
|
+
let payload;
|
|
1173
|
+
try {
|
|
1174
|
+
payload = JSON.parse(rawBody);
|
|
1175
|
+
}
|
|
1176
|
+
catch {
|
|
1177
|
+
return json({ error: 'Invalid JSON body' }, 400);
|
|
1178
|
+
}
|
|
1179
|
+
const eventId = typeof payload.eventId === 'string' ? payload.eventId : null;
|
|
1180
|
+
const eventKind = typeof payload.eventKind === 'string' ? payload.eventKind : null;
|
|
1181
|
+
const correlationId = typeof payload.correlationId === 'string' ? payload.correlationId : null;
|
|
1182
|
+
const triggerToken = typeof payload.triggerToken === 'string' ? payload.triggerToken : null;
|
|
1183
|
+
const nextState = typeof payload.nextState === 'string' ? payload.nextState : null;
|
|
1184
|
+
const errorMsg = typeof payload.error === 'string' ? payload.error : null;
|
|
1185
|
+
const transcript = typeof payload.transcript === 'string' ? payload.transcript : null;
|
|
1186
|
+
const durationSeconds = typeof payload.durationSeconds === 'number'
|
|
1187
|
+
? payload.durationSeconds
|
|
1188
|
+
: typeof payload.duration_seconds === 'number'
|
|
1189
|
+
? payload.duration_seconds
|
|
1190
|
+
: null;
|
|
1191
|
+
const description = typeof payload.description === 'string'
|
|
1192
|
+
? payload.description
|
|
1193
|
+
: typeof payload.summary === 'string'
|
|
1194
|
+
? payload.summary
|
|
1195
|
+
: null;
|
|
1196
|
+
const excerpt = typeof payload.excerpt === 'string'
|
|
1197
|
+
? payload.excerpt
|
|
1198
|
+
: typeof payload.summary === 'string'
|
|
1199
|
+
? payload.summary
|
|
1200
|
+
: null;
|
|
1201
|
+
if (!eventId || !eventKind || !correlationId || !triggerToken || !nextState) {
|
|
1202
|
+
return json({ error: 'Missing required callback fields' }, 400);
|
|
1203
|
+
}
|
|
1204
|
+
const ok = await applyFoundryCallback(ctx.db, {
|
|
1205
|
+
eventId,
|
|
1206
|
+
eventKind,
|
|
1207
|
+
correlationId,
|
|
1208
|
+
triggerToken,
|
|
1209
|
+
nextState: nextState,
|
|
1210
|
+
error: errorMsg,
|
|
1211
|
+
transcript,
|
|
1212
|
+
durationSeconds,
|
|
1213
|
+
description,
|
|
1214
|
+
excerpt,
|
|
1215
|
+
});
|
|
1216
|
+
// Fire video.processed / video.failed webhook on terminal Foundry states.
|
|
1217
|
+
if (nextState === 'ready') {
|
|
1218
|
+
await fireWebhook(ctx, 'video.processed', { correlationId, eventId, eventKind });
|
|
1219
|
+
}
|
|
1220
|
+
else if (nextState === 'failed') {
|
|
1221
|
+
await fireWebhook(ctx, 'video.failed', {
|
|
1222
|
+
correlationId,
|
|
1223
|
+
eventId,
|
|
1224
|
+
eventKind,
|
|
1225
|
+
error: errorMsg,
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
return json({ ok });
|
|
1229
|
+
},
|
|
1230
|
+
async seoScore(ctx) {
|
|
1231
|
+
const denied = await resolved.authz.requirePublisher(ctx);
|
|
1232
|
+
if (denied)
|
|
1233
|
+
return denied;
|
|
1234
|
+
const idResult = requireId(ctx);
|
|
1235
|
+
if ('error' in idResult)
|
|
1236
|
+
return idResult.error;
|
|
1237
|
+
const id = idResult.id;
|
|
1238
|
+
const item = await getContentItem(ctx.db, id);
|
|
1239
|
+
if (!item)
|
|
1240
|
+
return json({ error: 'Not found' }, 404);
|
|
1241
|
+
// Read the persisted body markdown for article / newsletter types.
|
|
1242
|
+
let persistedBodyMarkdown = null;
|
|
1243
|
+
if (item.type === 'article' || item.type === 'newsletter') {
|
|
1244
|
+
const row = await ctx.db
|
|
1245
|
+
.prepare('SELECT body_markdown FROM article_content WHERE content_id = ? LIMIT 1')
|
|
1246
|
+
.bind(id)
|
|
1247
|
+
.first();
|
|
1248
|
+
persistedBodyMarkdown = row?.body_markdown ?? null;
|
|
1249
|
+
}
|
|
1250
|
+
// Read the persisted focus keyword (migration 0004 column).
|
|
1251
|
+
const focusRow = await ctx.db
|
|
1252
|
+
.prepare('SELECT seo_focus_keyword FROM content_items WHERE id = ? LIMIT 1')
|
|
1253
|
+
.bind(id)
|
|
1254
|
+
.first();
|
|
1255
|
+
const persistedFocusKeyword = focusRow?.seo_focus_keyword ?? null;
|
|
1256
|
+
// Parse optional override fields from the request body.
|
|
1257
|
+
const rawBody = (await readJson(ctx));
|
|
1258
|
+
const overrides = rawBody && typeof rawBody === 'object' ? rawBody : {};
|
|
1259
|
+
const titleOverride = typeof overrides.title === 'string' ? overrides.title : undefined;
|
|
1260
|
+
const seoTitleOverride = typeof overrides.seoTitle === 'string' ? overrides.seoTitle : undefined;
|
|
1261
|
+
const descriptionOverride = typeof overrides.description === 'string' ? overrides.description : undefined;
|
|
1262
|
+
const focusKeywordOverride = typeof overrides.focusKeyword === 'string' ? overrides.focusKeyword : undefined;
|
|
1263
|
+
const bodyMarkdownOverride = typeof overrides.bodyMarkdown === 'string' ? overrides.bodyMarkdown : undefined;
|
|
1264
|
+
const input = {
|
|
1265
|
+
title: titleOverride ?? item.title,
|
|
1266
|
+
seoTitle: seoTitleOverride !== undefined ? seoTitleOverride : (item.seo_title ?? undefined),
|
|
1267
|
+
description: descriptionOverride !== undefined ? descriptionOverride : (item.description ?? undefined),
|
|
1268
|
+
focusKeyword: focusKeywordOverride !== undefined
|
|
1269
|
+
? focusKeywordOverride
|
|
1270
|
+
: (persistedFocusKeyword ?? undefined),
|
|
1271
|
+
bodyMarkdown: bodyMarkdownOverride !== undefined
|
|
1272
|
+
? bodyMarkdownOverride
|
|
1273
|
+
: (persistedBodyMarkdown ?? undefined),
|
|
1274
|
+
};
|
|
1275
|
+
const result = await resolved.providers.seo.scoreDraft(input);
|
|
1276
|
+
return json({ score: result.score, checks: result.checks });
|
|
1277
|
+
},
|
|
1278
|
+
async contributorsList(ctx) {
|
|
1279
|
+
const denied = await resolved.authz.requirePublisher(ctx);
|
|
1280
|
+
if (denied)
|
|
1281
|
+
return denied;
|
|
1282
|
+
const idResult = requireId(ctx);
|
|
1283
|
+
if ('error' in idResult)
|
|
1284
|
+
return idResult.error;
|
|
1285
|
+
const id = idResult.id;
|
|
1286
|
+
// 404 if the content item doesn't exist.
|
|
1287
|
+
const item = await getContentItem(ctx.db, id);
|
|
1288
|
+
if (!item)
|
|
1289
|
+
return json({ error: 'Not found' }, 404);
|
|
1290
|
+
const contributors = await listContributors(ctx.db, id);
|
|
1291
|
+
return json({ contributors });
|
|
1292
|
+
},
|
|
1293
|
+
async contributorsReplace(ctx) {
|
|
1294
|
+
const idResult = requireId(ctx);
|
|
1295
|
+
if ('error' in idResult)
|
|
1296
|
+
return idResult.error;
|
|
1297
|
+
const id = idResult.id;
|
|
1298
|
+
// 404 if the content item doesn't exist.
|
|
1299
|
+
const item = await getContentItem(ctx.db, id);
|
|
1300
|
+
if (!item)
|
|
1301
|
+
return json({ error: 'Not found' }, 404);
|
|
1302
|
+
// Editing the byline list is a write on the content item.
|
|
1303
|
+
const denied = await requireContentAction(ctx, 'write', item.author_id);
|
|
1304
|
+
if (denied)
|
|
1305
|
+
return denied;
|
|
1306
|
+
const parsed = ContributorsReplaceSchema.safeParse(await readJson(ctx));
|
|
1307
|
+
if (!parsed.success) {
|
|
1308
|
+
return json({ error: 'Invalid request', details: parsed.error.flatten() }, 400);
|
|
1309
|
+
}
|
|
1310
|
+
await setContributors(ctx.db, id, parsed.data.contributors.map((c) => ({
|
|
1311
|
+
// Prod's content_contributors.role is NOT NULL DEFAULT 'cohost'; an
|
|
1312
|
+
// omitted role must resolve to that default, never NULL. The engine
|
|
1313
|
+
// applies the same fallback as the load-bearing guard.
|
|
1314
|
+
authorId: c.authorId,
|
|
1315
|
+
role: c.role ?? 'cohost',
|
|
1316
|
+
position: c.position,
|
|
1317
|
+
})));
|
|
1318
|
+
const contributors = await listContributors(ctx.db, id);
|
|
1319
|
+
return json({ contributors });
|
|
1320
|
+
},
|
|
1321
|
+
async revisionsList(ctx) {
|
|
1322
|
+
const denied = await resolved.authz.requirePublisher(ctx);
|
|
1323
|
+
if (denied)
|
|
1324
|
+
return denied;
|
|
1325
|
+
const idResult = requireId(ctx);
|
|
1326
|
+
if ('error' in idResult)
|
|
1327
|
+
return idResult.error;
|
|
1328
|
+
const id = idResult.id;
|
|
1329
|
+
// 404 if the content item doesn't exist.
|
|
1330
|
+
const item = await getContentItem(ctx.db, id);
|
|
1331
|
+
if (!item)
|
|
1332
|
+
return json({ error: 'Not found' }, 404);
|
|
1333
|
+
const revisions = await listRevisions(ctx.db, id);
|
|
1334
|
+
return json({ revisions });
|
|
1335
|
+
},
|
|
1336
|
+
async revisionGet(ctx) {
|
|
1337
|
+
const denied = await resolved.authz.requirePublisher(ctx);
|
|
1338
|
+
if (denied)
|
|
1339
|
+
return denied;
|
|
1340
|
+
const idResult = requireId(ctx);
|
|
1341
|
+
if ('error' in idResult)
|
|
1342
|
+
return idResult.error;
|
|
1343
|
+
const id = idResult.id;
|
|
1344
|
+
const rev = ctx.params.rev;
|
|
1345
|
+
if (!rev)
|
|
1346
|
+
return json({ error: 'Missing rev' }, 400);
|
|
1347
|
+
const result = await getRevision(ctx.db, id, rev);
|
|
1348
|
+
if (!result)
|
|
1349
|
+
return json({ error: 'Not found' }, 404);
|
|
1350
|
+
return json({ summary: result.summary, payload: result.payload });
|
|
1351
|
+
},
|
|
1352
|
+
async revisionRestore(ctx) {
|
|
1353
|
+
const idResult = requireId(ctx);
|
|
1354
|
+
if ('error' in idResult)
|
|
1355
|
+
return idResult.error;
|
|
1356
|
+
const id = idResult.id;
|
|
1357
|
+
// Restoring a revision rewrites content fields → a write on the item.
|
|
1358
|
+
const existing = await getContentItem(ctx.db, id);
|
|
1359
|
+
if (!existing)
|
|
1360
|
+
return json({ error: 'Not found' }, 404);
|
|
1361
|
+
const denied = await requireContentAction(ctx, 'write', existing.author_id);
|
|
1362
|
+
if (denied)
|
|
1363
|
+
return denied;
|
|
1364
|
+
const rev = ctx.params.rev;
|
|
1365
|
+
if (!rev)
|
|
1366
|
+
return json({ error: 'Missing rev' }, 400);
|
|
1367
|
+
const ok = await restoreRevision(ctx.db, id, rev, { createdBy: ctx.userId ?? null });
|
|
1368
|
+
if (!ok)
|
|
1369
|
+
return json({ error: 'Not found' }, 404);
|
|
1370
|
+
return json({ restored: true });
|
|
1371
|
+
},
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
//# sourceMappingURL=content.js.map
|