@velora-cms/server 0.11.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/LICENSE +202 -0
- package/README.md +65 -0
- package/dist/admin-bundle/assets/__virtual_mf___mfe_internal__velora_admin__mf_owner__1__loadShare__react__loadShare__.js_commonjs-proxy-Ng0fOpdU.js +1 -0
- package/dist/admin-bundle/assets/__virtual_mf___mfe_internal__velora_admin__mf_owner__1__loadShare__react_mf_2_dom__loadShare__.js_commonjs-proxy-oc1i_iBU.js +9 -0
- package/dist/admin-bundle/assets/_virtual_mf-localSharedImportMap___mfe_internal__velora_admin__mf_owner__1-DOKdWYyj.js +1 -0
- package/dist/admin-bundle/assets/_virtual_mf___mfe_internal__velora_admin__mf_owner__1__loadShare___mf_0_velora_mf_2_cms_mf_1_plugin_mf_2_sdk__loadShare__.js-C1-ZAxwO.js +2 -0
- package/dist/admin-bundle/assets/_virtual_mf___mfe_internal__velora_admin__mf_owner__1__loadShare__react__loadShare__.js-BuUBskgj.js +9 -0
- package/dist/admin-bundle/assets/_virtual_mf___mfe_internal__velora_admin__mf_owner__1__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-DMaku-HC.js +9 -0
- package/dist/admin-bundle/assets/_virtual_mf___mfe_internal__velora_admin__mf_owner__1__loadShare__react_mf_2_dom__loadShare__.js-DYfqn12F.js +1 -0
- package/dist/admin-bundle/assets/_virtual_mf___mfe_internal__velora_admin__mf_owner__1__loadShare__react_mf_2_dom_mf_1_client__loadShare__.js-BPoj3DC9.js +25 -0
- package/dist/admin-bundle/assets/content-trash.schema-DfXHkC-w.js +72 -0
- package/dist/admin-bundle/assets/hostInit--C4xK6nj.js +1 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-arabic-arabic-400-normal-CyU-ddYS.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-arabic-arabic-400-normal-EA1o-u_Q.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-arabic-arabic-500-normal-C4MQITzh.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-arabic-arabic-500-normal-DPXcppbm.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-arabic-arabic-600-normal-0pRdybE_.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-arabic-arabic-600-normal-MJZ0liF5.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-400-normal-Bo5KPYvw.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-400-normal-CFZh1unN.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-500-normal-BletK1Hp.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-500-normal-Cd6jVIg7.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-600-normal-Dk5eISD_.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-600-normal-KrqB56Mw.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-ext-400-normal-BeVLg08x.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-ext-400-normal-BpLfq-63.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-ext-500-normal-Ck_W4G78.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-ext-500-normal-eJxPnL1P.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-ext-600-normal-DT29Q8HW.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-ext-600-normal-DjDAeNQD.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-400-normal-BTotfTJu.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-400-normal-DZqxrq2p.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-500-normal-ByOcLdNv.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-500-normal-CocWQlwt.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-600-normal-71GNu3SW.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-600-normal-BGq0mW3O.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-ext-400-normal-Dsrv2Tcn.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-ext-400-normal-g30qAdWV.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-ext-500-normal-Cs5J6C77.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-ext-500-normal-DB5PtV2g.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-ext-600-normal-Bz0x94Yp.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-ext-600-normal-DUMzJB7m.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-greek-400-normal-D9ESIMu3.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-greek-400-normal-_efipK4i.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-greek-500-normal-CuWXN6rf.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-greek-500-normal-JMMifIXV.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-greek-600-normal-D-CqTdkO.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-greek-600-normal-DzTrcv_p.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-latin-ext-400-normal-C5H60-Va.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-latin-ext-400-normal-RBey6euL.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-latin-ext-500-normal-D0aIdm-b.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-latin-ext-500-normal-DakdToA3.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-latin-ext-600-normal-DIrixKbi.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-latin-ext-600-normal-DOrvGEcy.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-vietnamese-400-normal-DG4YqDda.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-vietnamese-400-normal-fK1oJ5dG.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-vietnamese-500-normal-BEb3_waV.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-vietnamese-500-normal-e4dixQRQ.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-vietnamese-600-normal-DgdngZtN.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-vietnamese-600-normal-DpPYBSTl.woff2 +0 -0
- package/dist/admin-bundle/assets/index-BIAF0ShP.js +542 -0
- package/dist/admin-bundle/assets/index-DhMY-0sC.js +148 -0
- package/dist/admin-bundle/assets/index-DnZV_gsg.css +1 -0
- package/dist/admin-bundle/assets/index-Do71v6NZ.js +7 -0
- package/dist/admin-bundle/assets/literata-cyrillic-ext-opsz-italic-5yUt4G_9.woff2 +0 -0
- package/dist/admin-bundle/assets/literata-cyrillic-ext-opsz-normal-C5tfwALZ.woff2 +0 -0
- package/dist/admin-bundle/assets/literata-cyrillic-opsz-italic-Bj0ndbyp.woff2 +0 -0
- package/dist/admin-bundle/assets/literata-cyrillic-opsz-normal-DgmypKXX.woff2 +0 -0
- package/dist/admin-bundle/assets/literata-greek-ext-opsz-italic-DFpO5j-Z.woff2 +0 -0
- package/dist/admin-bundle/assets/literata-greek-ext-opsz-normal-DGm64vGR.woff2 +0 -0
- package/dist/admin-bundle/assets/literata-greek-opsz-italic-CHERNRFq.woff2 +0 -0
- package/dist/admin-bundle/assets/literata-greek-opsz-normal-DFglUmAd.woff2 +0 -0
- package/dist/admin-bundle/assets/literata-latin-ext-opsz-italic-4y8iAqqs.woff2 +0 -0
- package/dist/admin-bundle/assets/literata-latin-ext-opsz-normal-STiTPs0F.woff2 +0 -0
- package/dist/admin-bundle/assets/literata-latin-opsz-italic-CJ93D38p.woff2 +0 -0
- package/dist/admin-bundle/assets/literata-latin-opsz-normal-BHZsWIlc.woff2 +0 -0
- package/dist/admin-bundle/assets/literata-vietnamese-opsz-italic-BdcrZMcO.woff2 +0 -0
- package/dist/admin-bundle/assets/literata-vietnamese-opsz-normal-Gqw5JQlq.woff2 +0 -0
- package/dist/admin-bundle/assets/mf-entry-bootstrap-0-aef015cd.js +41 -0
- package/dist/admin-bundle/assets/remoteEntry-CNyN68bc.js +14 -0
- package/dist/admin-bundle/assets/virtualExposes-BDCSo5OS.js +1 -0
- package/dist/admin-bundle/assets/vite-preload-helper-Cgledu0r.js +1 -0
- package/dist/admin-bundle/index.html +16 -0
- package/dist/app.d.ts +8 -0
- package/dist/app.js +252 -0
- package/dist/auth/api-content-author.d.ts +2 -0
- package/dist/auth/api-content-author.js +27 -0
- package/dist/auth/api-keys.d.ts +7 -0
- package/dist/auth/api-keys.js +85 -0
- package/dist/auth/jwt-payload.d.ts +16 -0
- package/dist/auth/jwt-payload.js +1 -0
- package/dist/auth/preview-tokens.d.ts +7 -0
- package/dist/auth/preview-tokens.js +76 -0
- package/dist/auth/rate-limit.d.ts +16 -0
- package/dist/auth/rate-limit.js +283 -0
- package/dist/auth/require-auth.d.ts +4 -0
- package/dist/auth/require-auth.js +28 -0
- package/dist/auth/route-rate-limits.d.ts +31 -0
- package/dist/auth/route-rate-limits.js +155 -0
- package/dist/auth/tokens.d.ts +15 -0
- package/dist/auth/tokens.js +38 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +6 -0
- package/dist/boot/core-migration-gate.d.ts +12 -0
- package/dist/boot/core-migration-gate.js +52 -0
- package/dist/boot/resolve-boot-mode.d.ts +6 -0
- package/dist/boot/resolve-boot-mode.js +206 -0
- package/dist/bundled-templates/com.velora.blog/manifest.json +39 -0
- package/dist/bundled-templates/com.velora.blog/package.tar.gz +0 -0
- package/dist/bundled-templates/com.velora.blog/sample-content.json +14 -0
- package/dist/config/admin-ui.d.ts +1 -0
- package/dist/config/admin-ui.js +10 -0
- package/dist/config/cors.d.ts +7 -0
- package/dist/config/cors.js +53 -0
- package/dist/config/env-file-path.d.ts +2 -0
- package/dist/config/env-file-path.js +22 -0
- package/dist/config/graphql-limits.d.ts +2 -0
- package/dist/config/graphql-limits.js +26 -0
- package/dist/config/headless.d.ts +1 -0
- package/dist/config/headless.js +8 -0
- package/dist/config/license-revalidation.d.ts +2 -0
- package/dist/config/license-revalidation.js +16 -0
- package/dist/config/load-env.d.ts +1 -0
- package/dist/config/load-env.js +53 -0
- package/dist/config/marketplace.d.ts +11 -0
- package/dist/config/marketplace.js +19 -0
- package/dist/config/preview.d.ts +3 -0
- package/dist/config/preview.js +39 -0
- package/dist/config/public-docs.d.ts +1 -0
- package/dist/config/public-docs.js +9 -0
- package/dist/config/site.d.ts +7 -0
- package/dist/config/site.js +12 -0
- package/dist/config/trust-proxy.d.ts +2 -0
- package/dist/config/trust-proxy.js +42 -0
- package/dist/data-types/validate-data-type-settings.d.ts +1 -0
- package/dist/data-types/validate-data-type-settings.js +33 -0
- package/dist/db/content-db.d.ts +16 -0
- package/dist/db/content-db.js +109 -0
- package/dist/db/core-migrations/index.d.ts +14 -0
- package/dist/db/core-migrations/index.js +184 -0
- package/dist/db/core-migrations/steps.d.ts +4 -0
- package/dist/db/core-migrations/steps.js +66 -0
- package/dist/db/load-prisma-client.d.ts +2 -0
- package/dist/db/load-prisma-client.js +44 -0
- package/dist/db/prisma.d.ts +3 -0
- package/dist/db/prisma.js +67 -0
- package/dist/db/resolve-client-dir.d.ts +2 -0
- package/dist/db/resolve-client-dir.js +83 -0
- package/dist/docs/filter-public-api-spec.d.ts +2 -0
- package/dist/docs/filter-public-api-spec.js +52 -0
- package/dist/docs-bundle/404.html +41 -0
- package/dist/docs-bundle/_astro/MobileTableOfContents.astro_astro_type_script_index_0_lang.BcSo_yiZ.js +1 -0
- package/dist/docs-bundle/_astro/Search.astro_astro_type_script_index_0_lang.CavldbAs.js +1 -0
- package/dist/docs-bundle/_astro/TableOfContents.astro_astro_type_script_index_0_lang.Csloo1VZ.js +1 -0
- package/dist/docs-bundle/_astro/common.DTXA7sXM.css +3 -0
- package/dist/docs-bundle/_astro/ec.0vx5m.js +3 -0
- package/dist/docs-bundle/_astro/ec.w36nc.css +1 -0
- package/dist/docs-bundle/_astro/page.Dwipeu-R.js +1 -0
- package/dist/docs-bundle/_astro/print.ehPL0gv-.css +1 -0
- package/dist/docs-bundle/_astro/starlight-toc.CTCQXEge.js +1 -0
- package/dist/docs-bundle/_astro/ui-core.D-7Scnhs.js +2 -0
- package/dist/docs-bundle/favicon.svg +1 -0
- package/dist/docs-bundle/index.html +1 -0
- package/dist/docs-bundle/pagefind/fragment/en_18a36d7.pf_fragment +0 -0
- package/dist/docs-bundle/pagefind/fragment/en_1d9eb24.pf_fragment +0 -0
- package/dist/docs-bundle/pagefind/fragment/en_1ef320c.pf_fragment +0 -0
- package/dist/docs-bundle/pagefind/fragment/en_3099406.pf_fragment +0 -0
- package/dist/docs-bundle/pagefind/fragment/en_4a5b6fe.pf_fragment +0 -0
- package/dist/docs-bundle/pagefind/fragment/en_9aa652e.pf_fragment +0 -0
- package/dist/docs-bundle/pagefind/fragment/en_baccdf6.pf_fragment +0 -0
- package/dist/docs-bundle/pagefind/fragment/en_d81f3fe.pf_fragment +0 -0
- package/dist/docs-bundle/pagefind/fragment/en_d855550.pf_fragment +0 -0
- package/dist/docs-bundle/pagefind/fragment/en_e6d88d8.pf_fragment +0 -0
- package/dist/docs-bundle/pagefind/fragment/en_e9d3189.pf_fragment +0 -0
- package/dist/docs-bundle/pagefind/index/en_5feeed7.pf_index +0 -0
- package/dist/docs-bundle/pagefind/index/en_c6981f9.pf_index +0 -0
- package/dist/docs-bundle/pagefind/index/en_ce4f6bf.pf_index +0 -0
- package/dist/docs-bundle/pagefind/pagefind-component-ui.css +1509 -0
- package/dist/docs-bundle/pagefind/pagefind-component-ui.js +56 -0
- package/dist/docs-bundle/pagefind/pagefind-entry.json +1 -0
- package/dist/docs-bundle/pagefind/pagefind-highlight.js +1070 -0
- package/dist/docs-bundle/pagefind/pagefind-modular-ui.css +214 -0
- package/dist/docs-bundle/pagefind/pagefind-modular-ui.js +8 -0
- package/dist/docs-bundle/pagefind/pagefind-ui.css +1 -0
- package/dist/docs-bundle/pagefind/pagefind-ui.js +2 -0
- package/dist/docs-bundle/pagefind/pagefind-worker.js +6 -0
- package/dist/docs-bundle/pagefind/pagefind.en_1e94abd852.pf_meta +0 -0
- package/dist/docs-bundle/pagefind/pagefind.js +6 -0
- package/dist/docs-bundle/pagefind/wasm.en.pagefind +0 -0
- package/dist/docs-bundle/pagefind/wasm.unknown.pagefind +0 -0
- package/dist/docs-bundle/v1/index.html +86 -0
- package/dist/docs-bundle/v1/using-velora/build-a-headless-frontend/index.html +413 -0
- package/dist/docs-bundle/v1/using-velora/content-model/index.html +858 -0
- package/dist/docs-bundle/v1/using-velora/editing/index.html +748 -0
- package/dist/docs-bundle/v1/using-velora/getting-started/index.html +588 -0
- package/dist/docs-bundle/v1/using-velora/headless-api/index.html +1620 -0
- package/dist/docs-bundle/v1/using-velora/index.html +215 -0
- package/dist/docs-bundle/v1/using-velora/installation/index.html +886 -0
- package/dist/docs-bundle/v1/using-velora/installing-plugins/index.html +836 -0
- package/dist/docs-bundle/v1/using-velora/media/index.html +468 -0
- package/dist/docs-bundle/v1/using-velora/users-and-roles/index.html +561 -0
- package/dist/graphql/field-count-rule.d.ts +5 -0
- package/dist/graphql/field-count-rule.js +59 -0
- package/dist/graphql/schema.d.ts +12 -0
- package/dist/graphql/schema.js +330 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +180 -0
- package/dist/lib.d.ts +2 -0
- package/dist/lib.js +39 -0
- package/dist/marketplace/install-bundle-members.d.ts +33 -0
- package/dist/marketplace/install-bundle-members.js +137 -0
- package/dist/marketplace/install-from-marketplace.d.ts +60 -0
- package/dist/marketplace/install-from-marketplace.js +581 -0
- package/dist/marketplace/license-revalidation-job.d.ts +38 -0
- package/dist/marketplace/license-revalidation-job.js +139 -0
- package/dist/marketplace/marketplace-client.d.ts +43 -0
- package/dist/marketplace/marketplace-client.js +228 -0
- package/dist/marketplace/revalidate-license.d.ts +39 -0
- package/dist/marketplace/revalidate-license.js +103 -0
- package/dist/media/media-service.d.ts +36 -0
- package/dist/media/media-service.js +162 -0
- package/dist/plugins/boot-plugin-loader.d.ts +32 -0
- package/dist/plugins/boot-plugin-loader.js +57 -0
- package/dist/plugins/hook-dispatcher.d.ts +13 -0
- package/dist/plugins/hook-dispatcher.js +68 -0
- package/dist/plugins/install-built-package.d.ts +36 -0
- package/dist/plugins/install-built-package.js +199 -0
- package/dist/plugins/install-package-tarball.d.ts +25 -0
- package/dist/plugins/install-package-tarball.js +73 -0
- package/dist/plugins/plugin-context.d.ts +5 -0
- package/dist/plugins/plugin-context.js +26 -0
- package/dist/plugins/plugin-package-store.d.ts +31 -0
- package/dist/plugins/plugin-package-store.js +64 -0
- package/dist/plugins/plugin-registry-service.d.ts +227 -0
- package/dist/plugins/plugin-registry-service.js +636 -0
- package/dist/plugins/seed-template-content.d.ts +23 -0
- package/dist/plugins/seed-template-content.js +198 -0
- package/dist/plugins/server-serializers.d.ts +3 -0
- package/dist/plugins/server-serializers.js +16 -0
- package/dist/plugins/simple-storage-api.d.ts +9 -0
- package/dist/plugins/simple-storage-api.js +26 -0
- package/dist/plugins/snapshot-groups.d.ts +16 -0
- package/dist/plugins/snapshot-groups.js +81 -0
- package/dist/plugins/structured-query-builder.d.ts +21 -0
- package/dist/plugins/structured-query-builder.js +60 -0
- package/dist/plugins/structured-snapshot-service.d.ts +16 -0
- package/dist/plugins/structured-snapshot-service.js +286 -0
- package/dist/plugins/structured-storage-api.d.ts +22 -0
- package/dist/plugins/structured-storage-api.js +91 -0
- package/dist/plugins/structured-storage-transaction.d.ts +22 -0
- package/dist/plugins/structured-storage-transaction.js +63 -0
- package/dist/plugins/structured-table-naming.d.ts +1 -0
- package/dist/plugins/structured-table-naming.js +18 -0
- package/dist/plugins/trusted-keys.d.ts +11 -0
- package/dist/plugins/trusted-keys.js +67 -0
- package/dist/plugins/trusted-signing-keys.d.ts +27 -0
- package/dist/plugins/trusted-signing-keys.js +40 -0
- package/dist/plugins/uninstall-bundle.d.ts +29 -0
- package/dist/plugins/uninstall-bundle.js +150 -0
- package/dist/plugins/verify-package.d.ts +36 -0
- package/dist/plugins/verify-package.js +121 -0
- package/dist/routes/admin-static.d.ts +8 -0
- package/dist/routes/admin-static.js +196 -0
- package/dist/routes/api-keys.d.ts +2 -0
- package/dist/routes/api-keys.js +95 -0
- package/dist/routes/api-playground.d.ts +2 -0
- package/dist/routes/api-playground.js +21 -0
- package/dist/routes/auth.d.ts +2 -0
- package/dist/routes/auth.js +102 -0
- package/dist/routes/content.d.ts +3 -0
- package/dist/routes/content.js +471 -0
- package/dist/routes/data-types.d.ts +3 -0
- package/dist/routes/data-types.js +100 -0
- package/dist/routes/docs-static.d.ts +8 -0
- package/dist/routes/docs-static.js +137 -0
- package/dist/routes/document-types.d.ts +3 -0
- package/dist/routes/document-types.js +149 -0
- package/dist/routes/headless-bridge.d.ts +10 -0
- package/dist/routes/headless-bridge.js +207 -0
- package/dist/routes/health.d.ts +2 -0
- package/dist/routes/health.js +15 -0
- package/dist/routes/marketplace-proxy.d.ts +8 -0
- package/dist/routes/marketplace-proxy.js +135 -0
- package/dist/routes/media.d.ts +6 -0
- package/dist/routes/media.js +93 -0
- package/dist/routes/plugin-api-routes.d.ts +12 -0
- package/dist/routes/plugin-api-routes.js +224 -0
- package/dist/routes/plugin-assets.d.ts +8 -0
- package/dist/routes/plugin-assets.js +51 -0
- package/dist/routes/plugin-management.d.ts +7 -0
- package/dist/routes/plugin-management.js +525 -0
- package/dist/routes/plugin-storage.d.ts +8 -0
- package/dist/routes/plugin-storage.js +104 -0
- package/dist/routes/plugins.d.ts +2 -0
- package/dist/routes/plugins.js +36 -0
- package/dist/routes/public-api.d.ts +9 -0
- package/dist/routes/public-api.js +545 -0
- package/dist/routes/published-item.d.ts +57 -0
- package/dist/routes/published-item.js +236 -0
- package/dist/routes/setup.d.ts +9 -0
- package/dist/routes/setup.js +477 -0
- package/dist/routes/snapshots.d.ts +3 -0
- package/dist/routes/snapshots.js +65 -0
- package/dist/routes/templates.d.ts +3 -0
- package/dist/routes/templates.js +37 -0
- package/dist/routes/themes.d.ts +2 -0
- package/dist/routes/themes.js +30 -0
- package/dist/routes/users.d.ts +2 -0
- package/dist/routes/users.js +113 -0
- package/dist/sandbox/register-local-plugin.d.ts +36 -0
- package/dist/sandbox/register-local-plugin.js +108 -0
- package/dist/sandbox/seed-sandbox.d.ts +20 -0
- package/dist/sandbox/seed-sandbox.js +122 -0
- package/dist/scripts/copy-admin-bundle.d.ts +1 -0
- package/dist/scripts/copy-admin-bundle.js +24 -0
- package/dist/scripts/copy-docs-bundle.d.ts +1 -0
- package/dist/scripts/copy-docs-bundle.js +22 -0
- package/dist/scripts/create-api-key.d.ts +5 -0
- package/dist/scripts/create-api-key.js +56 -0
- package/dist/scripts/generate-prisma-clients.d.ts +6 -0
- package/dist/scripts/generate-prisma-clients.js +125 -0
- package/dist/scripts/pack-bundled-templates.d.ts +1 -0
- package/dist/scripts/pack-bundled-templates.js +87 -0
- package/dist/setup/build-connection-string.d.ts +19 -0
- package/dist/setup/build-connection-string.js +14 -0
- package/dist/setup/bundled-templates.d.ts +19 -0
- package/dist/setup/bundled-templates.js +121 -0
- package/dist/setup/ensure-prisma-client.d.ts +3 -0
- package/dist/setup/ensure-prisma-client.js +23 -0
- package/dist/setup/install-starter.d.ts +14 -0
- package/dist/setup/install-starter.js +76 -0
- package/dist/setup/normalize-database-url.d.ts +2 -0
- package/dist/setup/normalize-database-url.js +35 -0
- package/dist/setup/persist-env-config.d.ts +2 -0
- package/dist/setup/persist-env-config.js +63 -0
- package/dist/setup/provision-database.d.ts +2 -0
- package/dist/setup/provision-database.js +89 -0
- package/dist/setup-app.d.ts +4 -0
- package/dist/setup-app.js +43 -0
- package/package.json +96 -0
- package/prisma/schema.prisma +303 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ApiErrorSchema } from "@velora-cms/api-schemas";
|
|
3
|
+
import { requireAdmin } from "../auth/require-auth.js";
|
|
4
|
+
import { filterPublicApiSpec } from "../docs/filter-public-api-spec.js";
|
|
5
|
+
// Admin-only source of the public /api/v1 spec for the API playground
|
|
6
|
+
// (Session 109). Deliberately separate from the public docs surface so the
|
|
7
|
+
// admin tool works regardless of the VELORA_PUBLIC_DOCS off-switch. The
|
|
8
|
+
// response is an arbitrary OpenAPI object, so it's documented with a loose
|
|
9
|
+
// passthrough schema rather than a hand-modelled OpenAPI Zod type.
|
|
10
|
+
export const apiPlaygroundRoutes = async (app) => {
|
|
11
|
+
app.get("/api/api-playground/openapi", {
|
|
12
|
+
onRequest: requireAdmin,
|
|
13
|
+
schema: {
|
|
14
|
+
response: { 200: z.record(z.string(), z.unknown()), 401: ApiErrorSchema, 403: ApiErrorSchema },
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
// app.swagger() returns @fastify/swagger's Document type, which has no
|
|
18
|
+
// index signature — same plain-object shape as filterPublicApiSpec's
|
|
19
|
+
// OpenAPISpec, just a differently-shaped type declaration.
|
|
20
|
+
async () => filterPublicApiSpec(app.swagger()));
|
|
21
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { AuthErrorSchema, LoginRequestSchema, LoginResponseSchema, LogoutResponseSchema, RefreshResponseSchema, } from "@velora-cms/api-schemas";
|
|
2
|
+
import { prisma } from "../db/prisma.js";
|
|
3
|
+
import { rateLimitConfig } from "../auth/rate-limit.js";
|
|
4
|
+
import { LOGIN_RATE_LIMIT, ipOnlyKeyGenerator } from "../auth/route-rate-limits.js";
|
|
5
|
+
import { ACCESS_TOKEN_TTL, generateRefreshToken, hashRefreshToken, refreshCookieOptions, refreshTokenExpiryDate, REFRESH_COOKIE_NAME, verifyPassword, } from "../auth/tokens.js";
|
|
6
|
+
export const authRoutes = async (app) => {
|
|
7
|
+
// S162: this scope had NO ceiling at all — POST /api/auth/login is
|
|
8
|
+
// unauthenticated and is the system's most obvious brute-force target.
|
|
9
|
+
// Registered per-scope, matching the precedent in plugin-api-routes.ts
|
|
10
|
+
// and public-api.ts: @fastify/rate-limit decorates the instance, so it
|
|
11
|
+
// is registered in child scopes OR at the root, never both.
|
|
12
|
+
await app.register(import("@fastify/rate-limit"), rateLimitConfig);
|
|
13
|
+
app.post("/api/auth/login", {
|
|
14
|
+
schema: {
|
|
15
|
+
body: LoginRequestSchema,
|
|
16
|
+
response: {
|
|
17
|
+
200: LoginResponseSchema,
|
|
18
|
+
401: AuthErrorSchema,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
config: {
|
|
22
|
+
rateLimit: {
|
|
23
|
+
max: LOGIN_RATE_LIMIT.max(),
|
|
24
|
+
timeWindow: LOGIN_RATE_LIMIT.timeWindow(),
|
|
25
|
+
// IP-only: see ipOnlyKeyGenerator's comment. Overriding the
|
|
26
|
+
// shared generator here is the whole point — the default would
|
|
27
|
+
// put a token-bearing request in the unlimited jwt: lane.
|
|
28
|
+
keyGenerator: ipOnlyKeyGenerator,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
}, async (request, reply) => {
|
|
32
|
+
const { email, password } = request.body;
|
|
33
|
+
const user = await prisma.user.findUnique({ where: { email } });
|
|
34
|
+
if (!user || !(await verifyPassword(password, user.passwordHash))) {
|
|
35
|
+
return reply.code(401).send({ message: "Invalid email or password" });
|
|
36
|
+
}
|
|
37
|
+
const { accessToken } = await issueSession(app, reply, user);
|
|
38
|
+
return {
|
|
39
|
+
accessToken,
|
|
40
|
+
user: { id: user.id, email: user.email, name: user.name, role: user.role },
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
app.post("/api/auth/refresh", {
|
|
44
|
+
schema: {
|
|
45
|
+
response: {
|
|
46
|
+
200: RefreshResponseSchema,
|
|
47
|
+
401: AuthErrorSchema,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
}, async (request, reply) => {
|
|
51
|
+
const currentRefreshToken = request.cookies[REFRESH_COOKIE_NAME];
|
|
52
|
+
if (!currentRefreshToken) {
|
|
53
|
+
return reply.code(401).send({ message: "No refresh session" });
|
|
54
|
+
}
|
|
55
|
+
const tokenHash = hashRefreshToken(currentRefreshToken);
|
|
56
|
+
const session = await prisma.session.findUnique({ where: { refreshTokenHash: tokenHash } });
|
|
57
|
+
if (!session || session.revokedAt || session.expiresAt < new Date()) {
|
|
58
|
+
reply.clearCookie(REFRESH_COOKIE_NAME, { path: "/api/auth" });
|
|
59
|
+
return reply.code(401).send({ message: "Invalid or expired refresh token" });
|
|
60
|
+
}
|
|
61
|
+
const user = await prisma.user.findUniqueOrThrow({ where: { id: session.userId } });
|
|
62
|
+
// Rotate: revoke the session this token belonged to, then issue a
|
|
63
|
+
// brand-new one — a leaked refresh token can't be replayed after use.
|
|
64
|
+
await prisma.session.update({ where: { id: session.id }, data: { revokedAt: new Date() } });
|
|
65
|
+
const { accessToken } = await issueSession(app, reply, user);
|
|
66
|
+
return {
|
|
67
|
+
accessToken,
|
|
68
|
+
user: { id: user.id, email: user.email, name: user.name, role: user.role },
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
app.post("/api/auth/logout", {
|
|
72
|
+
schema: {
|
|
73
|
+
response: {
|
|
74
|
+
200: LogoutResponseSchema,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
}, async (request, reply) => {
|
|
78
|
+
const currentRefreshToken = request.cookies[REFRESH_COOKIE_NAME];
|
|
79
|
+
if (currentRefreshToken) {
|
|
80
|
+
const tokenHash = hashRefreshToken(currentRefreshToken);
|
|
81
|
+
await prisma.session.updateMany({
|
|
82
|
+
where: { refreshTokenHash: tokenHash, revokedAt: null },
|
|
83
|
+
data: { revokedAt: new Date() },
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
reply.clearCookie(REFRESH_COOKIE_NAME, { path: "/api/auth" });
|
|
87
|
+
return { success: true };
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
async function issueSession(app, reply, user) {
|
|
91
|
+
const accessToken = app.jwt.sign({ sub: user.id, email: user.email, role: user.role }, { expiresIn: ACCESS_TOKEN_TTL });
|
|
92
|
+
const refreshToken = generateRefreshToken();
|
|
93
|
+
await prisma.session.create({
|
|
94
|
+
data: {
|
|
95
|
+
userId: user.id,
|
|
96
|
+
refreshTokenHash: hashRefreshToken(refreshToken),
|
|
97
|
+
expiresAt: refreshTokenExpiryDate(),
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
reply.setCookie(REFRESH_COOKIE_NAME, refreshToken, refreshCookieOptions());
|
|
101
|
+
return { accessToken };
|
|
102
|
+
}
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ApiErrorSchema, ContentByPathQuerySchema, ContentByPathResponseSchema, ContentChildrenQuerySchema, ContentChildrenResponseSchema, ContentItemQuerySchema, ContentItemResponseSchema, ContentRestoreBlockedSchema, ContentRestoreResultSchema, ContentTrashBodySchema, ContentTrashConfirmationSchema, ContentTrashListQuerySchema, ContentTrashListResponseSchema, ContentTrashResultSchema, ContentValidationErrorSchema, CreateContentRequestSchema, LatestVersionResponseSchema, VersionListQuerySchema, VersionListResponseSchema, PreviewTokenResponseSchema, PublishContentRequestSchema, PublishContentResponseSchema, ReorderNodesRequestSchema, ReorderNodesResponseSchema, SaveContentRequestSchema, SaveContentResponseSchema, } from "@velora-cms/api-schemas";
|
|
3
|
+
import { ContentValidationError, DocumentTypeNotFoundForContentError, ParentContentNotFoundError, RestoreBlockedError, TreeRulesViolationError, } from "@velora-cms/content-service";
|
|
4
|
+
import { mintPreviewToken } from "../auth/preview-tokens.js";
|
|
5
|
+
import { requireAdmin, requireAuth } from "../auth/require-auth.js";
|
|
6
|
+
import { buildPreviewUrl } from "../config/preview.js";
|
|
7
|
+
const ContentParamsSchema = z.object({ id: z.string().uuid() });
|
|
8
|
+
// ContentService signals a missing content id / version number with a
|
|
9
|
+
// throw (same style throughout publishContent) — routes translate exactly
|
|
10
|
+
// those into 404s and let anything else stay a 500. There is no separate
|
|
11
|
+
// rollback route: POST /:id/publish with an explicit `version` repoints
|
|
12
|
+
// current at that older version, which is the rollback path.
|
|
13
|
+
function isNotFoundError(error) {
|
|
14
|
+
return (error instanceof Error &&
|
|
15
|
+
/no (content|version \d+) found/i.test(error.message));
|
|
16
|
+
}
|
|
17
|
+
export const contentRoutes = async (app, opts) => {
|
|
18
|
+
const { adapter, contentService } = opts;
|
|
19
|
+
// S156: the admin origin's honest-headless view resolves a pasted URL to a
|
|
20
|
+
// node. requireAuth (not requireAdmin) — it's a read, like this file's
|
|
21
|
+
// other GETs; drafts are visible because this is the back office.
|
|
22
|
+
// No locale/fallback params by decision: a URL names one node, and the
|
|
23
|
+
// adapter's default-locale-first tiebreak IS the resolution semantic here
|
|
24
|
+
// (a recorded deviation from api-design.md's locale-params convention).
|
|
25
|
+
app.get("/api/content/by-path", {
|
|
26
|
+
onRequest: requireAuth,
|
|
27
|
+
schema: {
|
|
28
|
+
querystring: ContentByPathQuerySchema,
|
|
29
|
+
response: {
|
|
30
|
+
200: ContentByPathResponseSchema,
|
|
31
|
+
401: ApiErrorSchema,
|
|
32
|
+
404: ApiErrorSchema,
|
|
33
|
+
500: ApiErrorSchema,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
}, async (request, reply) => {
|
|
37
|
+
const node = await adapter.getContentNodeByPath(request.query.path);
|
|
38
|
+
if (!node) {
|
|
39
|
+
return reply.code(404).send({ message: `No content found at path ${request.query.path}` });
|
|
40
|
+
}
|
|
41
|
+
return { node };
|
|
42
|
+
});
|
|
43
|
+
app.get("/api/content/:id", {
|
|
44
|
+
onRequest: requireAuth,
|
|
45
|
+
schema: {
|
|
46
|
+
params: ContentParamsSchema,
|
|
47
|
+
querystring: ContentItemQuerySchema,
|
|
48
|
+
response: {
|
|
49
|
+
200: ContentItemResponseSchema,
|
|
50
|
+
401: ApiErrorSchema,
|
|
51
|
+
404: ApiErrorSchema,
|
|
52
|
+
500: ApiErrorSchema,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
}, async (request, reply) => {
|
|
56
|
+
let node = await adapter.getContentNode(request.params.id);
|
|
57
|
+
if (!node) {
|
|
58
|
+
return reply.code(404).send({ message: `No content found for id ${request.params.id}` });
|
|
59
|
+
}
|
|
60
|
+
// ?locale=fr&fallback=en resolves through the node's translation
|
|
61
|
+
// group. The fallback decision itself lives in ContentService, not
|
|
62
|
+
// here (database.md) — without an explicit fallback param, only an
|
|
63
|
+
// exact translation matches.
|
|
64
|
+
if (request.query.locale) {
|
|
65
|
+
try {
|
|
66
|
+
node = await contentService.getByLocale(node.translationGroupId, request.query.locale, {
|
|
67
|
+
fallback: request.query.fallback ?? false,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (isNotFoundError(error)) {
|
|
72
|
+
return reply.code(404).send({ message: error.message });
|
|
73
|
+
}
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const version = await adapter.getContentVersion(node.id, node.currentVersion);
|
|
78
|
+
if (!version) {
|
|
79
|
+
// current_version pointing at a missing row is a broken invariant,
|
|
80
|
+
// not a caller mistake.
|
|
81
|
+
return reply.code(500).send({
|
|
82
|
+
message: `current_version (${node.currentVersion}) for ${node.id} has no matching content version`,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return { node, version };
|
|
86
|
+
});
|
|
87
|
+
app.get("/api/content/roots", {
|
|
88
|
+
onRequest: requireAuth,
|
|
89
|
+
schema: {
|
|
90
|
+
querystring: ContentChildrenQuerySchema,
|
|
91
|
+
response: {
|
|
92
|
+
200: ContentChildrenResponseSchema,
|
|
93
|
+
401: ApiErrorSchema,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
}, async (request) => {
|
|
97
|
+
const { cursor, limit } = request.query;
|
|
98
|
+
const items = await adapter.getRootNodes({ cursor, limit });
|
|
99
|
+
const nextCursor = items.length === limit ? (items[items.length - 1]?.id ?? null) : null;
|
|
100
|
+
return { items, nextCursor };
|
|
101
|
+
});
|
|
102
|
+
app.get("/api/content/:id/children", {
|
|
103
|
+
onRequest: requireAuth,
|
|
104
|
+
schema: {
|
|
105
|
+
params: ContentParamsSchema,
|
|
106
|
+
querystring: ContentChildrenQuerySchema,
|
|
107
|
+
response: {
|
|
108
|
+
200: ContentChildrenResponseSchema,
|
|
109
|
+
401: ApiErrorSchema,
|
|
110
|
+
404: ApiErrorSchema,
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
}, async (request, reply) => {
|
|
114
|
+
const parent = await adapter.getContentNode(request.params.id);
|
|
115
|
+
if (!parent) {
|
|
116
|
+
return reply.code(404).send({ message: `No content found for id ${request.params.id}` });
|
|
117
|
+
}
|
|
118
|
+
const { cursor, limit } = request.query;
|
|
119
|
+
const items = await adapter.getChildren(parent.id, { cursor, limit });
|
|
120
|
+
// A short page means the walk is done; a full page hands back the last
|
|
121
|
+
// id as the next cursor (an exactly-full final page just costs one
|
|
122
|
+
// extra empty-page request).
|
|
123
|
+
const nextCursor = items.length === limit ? (items[items.length - 1]?.id ?? null) : null;
|
|
124
|
+
return { items, nextCursor };
|
|
125
|
+
});
|
|
126
|
+
app.post("/api/content/roots/reorder", {
|
|
127
|
+
onRequest: requireAuth,
|
|
128
|
+
schema: {
|
|
129
|
+
body: ReorderNodesRequestSchema,
|
|
130
|
+
response: {
|
|
131
|
+
200: ReorderNodesResponseSchema,
|
|
132
|
+
400: ApiErrorSchema,
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
}, async (request, reply) => {
|
|
136
|
+
try {
|
|
137
|
+
await adapter.reorderSiblings(null, request.body.orderedIds);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
return reply.code(400).send({ message: error instanceof Error ? error.message : "Reorder failed" });
|
|
141
|
+
}
|
|
142
|
+
return { success: true };
|
|
143
|
+
});
|
|
144
|
+
app.post("/api/content/:id/children/reorder", {
|
|
145
|
+
onRequest: requireAuth,
|
|
146
|
+
schema: {
|
|
147
|
+
params: ContentParamsSchema,
|
|
148
|
+
body: ReorderNodesRequestSchema,
|
|
149
|
+
response: {
|
|
150
|
+
200: ReorderNodesResponseSchema,
|
|
151
|
+
400: ApiErrorSchema,
|
|
152
|
+
404: ApiErrorSchema,
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
}, async (request, reply) => {
|
|
156
|
+
const parent = await adapter.getContentNode(request.params.id);
|
|
157
|
+
if (!parent) {
|
|
158
|
+
return reply.code(404).send({ message: `No content found for id ${request.params.id}` });
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
await adapter.reorderSiblings(parent.id, request.body.orderedIds);
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
return reply.code(400).send({ message: error instanceof Error ? error.message : "Reorder failed" });
|
|
165
|
+
}
|
|
166
|
+
return { success: true };
|
|
167
|
+
});
|
|
168
|
+
// The newest saved version, regardless of the current_version pointer —
|
|
169
|
+
// GET /api/content/:id deliberately serves the pointer version, so this
|
|
170
|
+
// is the only way to read an unpublished draft (what the content editor
|
|
171
|
+
// edits). Session-gated like the other legacy content reads as of
|
|
172
|
+
// Session 110.
|
|
173
|
+
app.get("/api/content/:id/versions/latest", {
|
|
174
|
+
onRequest: requireAuth,
|
|
175
|
+
schema: {
|
|
176
|
+
params: ContentParamsSchema,
|
|
177
|
+
response: {
|
|
178
|
+
200: LatestVersionResponseSchema,
|
|
179
|
+
401: ApiErrorSchema,
|
|
180
|
+
404: ApiErrorSchema,
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
}, async (request, reply) => {
|
|
184
|
+
const version = await adapter.getLatestContentVersion(request.params.id);
|
|
185
|
+
if (!version) {
|
|
186
|
+
return reply.code(404).send({ message: `No content found for id ${request.params.id}` });
|
|
187
|
+
}
|
|
188
|
+
return { version };
|
|
189
|
+
});
|
|
190
|
+
// Version history, newest first, metadata only (no data payload).
|
|
191
|
+
// Version numbers are contiguous 1..latest and rows are never deleted
|
|
192
|
+
// (immutability), so the page enumerates numbers and resolves each via
|
|
193
|
+
// the unique (content_id, version) index — no list method on the
|
|
194
|
+
// adapter needed. If history views ever page through thousands of
|
|
195
|
+
// versions, a real listContentVersions adapter method replaces this
|
|
196
|
+
// loop; at a history page size of ≤100 point lookups it's not worth
|
|
197
|
+
// the interface change yet.
|
|
198
|
+
app.get("/api/content/:id/versions", {
|
|
199
|
+
onRequest: requireAuth,
|
|
200
|
+
schema: {
|
|
201
|
+
params: ContentParamsSchema,
|
|
202
|
+
querystring: VersionListQuerySchema,
|
|
203
|
+
response: {
|
|
204
|
+
200: VersionListResponseSchema,
|
|
205
|
+
401: ApiErrorSchema,
|
|
206
|
+
404: ApiErrorSchema,
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
}, async (request, reply) => {
|
|
210
|
+
const latest = await adapter.getLatestContentVersion(request.params.id);
|
|
211
|
+
if (!latest) {
|
|
212
|
+
return reply.code(404).send({ message: `No content found for id ${request.params.id}` });
|
|
213
|
+
}
|
|
214
|
+
const { cursor, limit } = request.query;
|
|
215
|
+
const start = cursor ? Math.min(cursor - 1, latest.version) : latest.version;
|
|
216
|
+
const numbers = [];
|
|
217
|
+
for (let n = start; n >= 1 && numbers.length < limit; n--)
|
|
218
|
+
numbers.push(n);
|
|
219
|
+
const items = [];
|
|
220
|
+
for (const n of numbers) {
|
|
221
|
+
const full = await adapter.getContentVersion(request.params.id, n);
|
|
222
|
+
if (full) {
|
|
223
|
+
const { data: _data, ...meta } = full;
|
|
224
|
+
items.push(meta);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const last = items[items.length - 1];
|
|
228
|
+
const nextCursor = last && last.version > 1 ? last.version : null;
|
|
229
|
+
return { items, nextCursor };
|
|
230
|
+
});
|
|
231
|
+
// One specific version, full data — the history view's "see what it
|
|
232
|
+
// looked like before" read.
|
|
233
|
+
app.get("/api/content/:id/versions/:version", {
|
|
234
|
+
onRequest: requireAuth,
|
|
235
|
+
schema: {
|
|
236
|
+
params: ContentParamsSchema.extend({
|
|
237
|
+
version: z.coerce.number().int().positive(),
|
|
238
|
+
}),
|
|
239
|
+
response: {
|
|
240
|
+
200: LatestVersionResponseSchema,
|
|
241
|
+
401: ApiErrorSchema,
|
|
242
|
+
404: ApiErrorSchema,
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
}, async (request, reply) => {
|
|
246
|
+
const version = await adapter.getContentVersion(request.params.id, request.params.version);
|
|
247
|
+
if (!version) {
|
|
248
|
+
return reply.code(404).send({
|
|
249
|
+
message: `No version ${request.params.version} found for content ${request.params.id}`,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
return { version };
|
|
253
|
+
});
|
|
254
|
+
app.post("/api/content", {
|
|
255
|
+
onRequest: requireAuth,
|
|
256
|
+
schema: {
|
|
257
|
+
body: CreateContentRequestSchema,
|
|
258
|
+
response: {
|
|
259
|
+
201: ContentItemResponseSchema,
|
|
260
|
+
// Union so both bodies serialize on the one 400 slot: the
|
|
261
|
+
// structured field-validation body, and the plain-message body
|
|
262
|
+
// Fastify's own Zod body validation produces (a bare
|
|
263
|
+
// ContentValidationErrorSchema turned those into a 500).
|
|
264
|
+
400: z.union([ContentValidationErrorSchema, ApiErrorSchema]),
|
|
265
|
+
401: ApiErrorSchema,
|
|
266
|
+
404: ApiErrorSchema,
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
}, async (request, reply) => {
|
|
270
|
+
try {
|
|
271
|
+
const result = await contentService.createContent({
|
|
272
|
+
...request.body,
|
|
273
|
+
createdBy: request.user.sub,
|
|
274
|
+
});
|
|
275
|
+
return reply.code(201).send(result);
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
if (error instanceof ContentValidationError) {
|
|
279
|
+
return reply.code(400).send({ message: error.message, fieldErrors: error.failures });
|
|
280
|
+
}
|
|
281
|
+
if (error instanceof ParentContentNotFoundError) {
|
|
282
|
+
return reply.code(404).send({ message: error.message });
|
|
283
|
+
}
|
|
284
|
+
if (error instanceof DocumentTypeNotFoundForContentError || error instanceof TreeRulesViolationError) {
|
|
285
|
+
return reply.code(400).send({ message: error.message });
|
|
286
|
+
}
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
app.put("/api/content/:id", {
|
|
291
|
+
onRequest: requireAuth,
|
|
292
|
+
schema: {
|
|
293
|
+
params: ContentParamsSchema,
|
|
294
|
+
body: SaveContentRequestSchema,
|
|
295
|
+
response: {
|
|
296
|
+
200: SaveContentResponseSchema,
|
|
297
|
+
400: z.union([ContentValidationErrorSchema, ApiErrorSchema]),
|
|
298
|
+
401: ApiErrorSchema,
|
|
299
|
+
404: ApiErrorSchema,
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
}, async (request, reply) => {
|
|
303
|
+
try {
|
|
304
|
+
const version = await contentService.saveContent(request.params.id, request.body.data, request.user.sub);
|
|
305
|
+
return { version };
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
if (error instanceof ContentValidationError) {
|
|
309
|
+
return reply.code(400).send({ message: error.message, fieldErrors: error.failures });
|
|
310
|
+
}
|
|
311
|
+
if (isNotFoundError(error)) {
|
|
312
|
+
return reply.code(404).send({ message: error.message });
|
|
313
|
+
}
|
|
314
|
+
throw error;
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
app.post("/api/content/:id/publish", {
|
|
318
|
+
onRequest: requireAuth,
|
|
319
|
+
schema: {
|
|
320
|
+
params: ContentParamsSchema,
|
|
321
|
+
// Optional body: absent (or {}) publishes the latest saved version.
|
|
322
|
+
// Fastify hands a bodyless POST to validation as null, not
|
|
323
|
+
// undefined — hence nullish, not optional.
|
|
324
|
+
body: PublishContentRequestSchema.nullish(),
|
|
325
|
+
response: {
|
|
326
|
+
200: PublishContentResponseSchema,
|
|
327
|
+
401: ApiErrorSchema,
|
|
328
|
+
404: ApiErrorSchema,
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
}, async (request, reply) => {
|
|
332
|
+
try {
|
|
333
|
+
const node = await contentService.publishContent(request.params.id, request.body?.version);
|
|
334
|
+
return { node };
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
if (isNotFoundError(error)) {
|
|
338
|
+
return reply.code(404).send({ message: error.message });
|
|
339
|
+
}
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
// S161: the trash browse surface. Admin-gated like DELETE below (and
|
|
344
|
+
// unlike this file's requireAuth routes) — trash operations are
|
|
345
|
+
// destructive-adjacent even though reversible. This resolves correctly
|
|
346
|
+
// against GET /api/content/:id (registered earlier in this file, not
|
|
347
|
+
// later) regardless of registration order: find-my-way ranks a static
|
|
348
|
+
// path segment ("trash") above a parametric one ("/:id") when matching,
|
|
349
|
+
// and ContentParamsSchema requires :id to be a uuid, which the literal
|
|
350
|
+
// string "trash" never is. Registration order is not the mechanism here.
|
|
351
|
+
app.get("/api/content/trash", {
|
|
352
|
+
onRequest: requireAdmin,
|
|
353
|
+
schema: {
|
|
354
|
+
querystring: ContentTrashListQuerySchema,
|
|
355
|
+
response: {
|
|
356
|
+
200: ContentTrashListResponseSchema,
|
|
357
|
+
401: ApiErrorSchema,
|
|
358
|
+
403: ApiErrorSchema,
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
}, async (request, reply) => {
|
|
362
|
+
const { cursor, limit } = request.query;
|
|
363
|
+
const items = await adapter.listTrashedSubtrees({ cursor, limit });
|
|
364
|
+
// A full page means there may be more; a short page is the end. Same
|
|
365
|
+
// convention as this file's other cursor-paginated reads.
|
|
366
|
+
const last = items.at(-1);
|
|
367
|
+
const nextCursor = items.length === limit && last ? `${last.deletedAt}|${last.id}` : null;
|
|
368
|
+
return reply.code(200).send({ items, nextCursor });
|
|
369
|
+
});
|
|
370
|
+
// S154: content's first delete surface. Two-step like the structured-data
|
|
371
|
+
// uninstall gate — unconfirmed 409 names the blast radius (the whole
|
|
372
|
+
// subtree, not just this node), confirmed trashes it. Soft delete only:
|
|
373
|
+
// reversible via the restore route below. Admin-gated (not just
|
|
374
|
+
// requireAuth, unlike this file's other routes) since it's destructive
|
|
375
|
+
// even though reversible.
|
|
376
|
+
app.delete("/api/content/:id", {
|
|
377
|
+
onRequest: requireAdmin,
|
|
378
|
+
schema: {
|
|
379
|
+
params: ContentParamsSchema,
|
|
380
|
+
body: ContentTrashBodySchema,
|
|
381
|
+
response: {
|
|
382
|
+
200: ContentTrashResultSchema,
|
|
383
|
+
401: ApiErrorSchema,
|
|
384
|
+
403: ApiErrorSchema,
|
|
385
|
+
404: ApiErrorSchema,
|
|
386
|
+
409: ContentTrashConfirmationSchema,
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
}, async (request, reply) => {
|
|
390
|
+
const node = await adapter.getContentNode(request.params.id);
|
|
391
|
+
if (!node) {
|
|
392
|
+
return reply.code(404).send({ message: `No content found for id ${request.params.id}` });
|
|
393
|
+
}
|
|
394
|
+
if (request.body?.confirm !== true) {
|
|
395
|
+
const subtreeCount = await adapter.countContentSubtree(request.params.id);
|
|
396
|
+
// Clamped defensively: countContentSubtree only counts LIVE rows,
|
|
397
|
+
// so if the target itself somehow isn't live when this executes
|
|
398
|
+
// (e.g. trashed out from under this request between the
|
|
399
|
+
// getContentNode check above and here), subtreeCount could be 0 —
|
|
400
|
+
// and 0 - 1 would render as "-1 descendant(s)" in the 409 body.
|
|
401
|
+
const descendantCount = Math.max(0, subtreeCount - 1);
|
|
402
|
+
return reply.code(409).send({
|
|
403
|
+
requiresConfirmation: true,
|
|
404
|
+
// ContentNode has no display-name field of its own — path is
|
|
405
|
+
// the closest thing to one.
|
|
406
|
+
nodeName: node.path,
|
|
407
|
+
descendantCount,
|
|
408
|
+
message: `deleting "${node.path}" moves it and ${descendantCount} descendant(s) to the trash — retry with confirm: true`,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
const result = await contentService.trashContent(request.params.id);
|
|
412
|
+
return reply.code(200).send(result);
|
|
413
|
+
});
|
|
414
|
+
app.post("/api/content/:id/restore", {
|
|
415
|
+
onRequest: requireAdmin,
|
|
416
|
+
schema: {
|
|
417
|
+
params: ContentParamsSchema,
|
|
418
|
+
response: {
|
|
419
|
+
200: ContentRestoreResultSchema,
|
|
420
|
+
401: ApiErrorSchema,
|
|
421
|
+
403: ApiErrorSchema,
|
|
422
|
+
404: ApiErrorSchema,
|
|
423
|
+
409: ContentRestoreBlockedSchema,
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
}, async (request, reply) => {
|
|
427
|
+
try {
|
|
428
|
+
const result = await contentService.restoreContent(request.params.id);
|
|
429
|
+
return reply.code(200).send(result);
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
432
|
+
if (error instanceof RestoreBlockedError) {
|
|
433
|
+
return reply.code(409).send({
|
|
434
|
+
message: error.message,
|
|
435
|
+
blockingAncestorPath: error.blockingAncestorPath,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
if (isNotFoundError(error)) {
|
|
439
|
+
return reply.code(404).send({ message: error.message });
|
|
440
|
+
}
|
|
441
|
+
throw error;
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
// Session 116: mints a stateless, short-TTL, single-content-id preview
|
|
445
|
+
// token (apps/server/src/auth/preview-tokens.ts) that unlocks a draft
|
|
446
|
+
// read on the public content API for exactly this id. Any existing
|
|
447
|
+
// content id may be minted for, published or not — a session holder is
|
|
448
|
+
// a trusted editor previewing their own work, not an anonymous reader.
|
|
449
|
+
app.post("/api/content/:id/preview-token", {
|
|
450
|
+
onRequest: requireAuth,
|
|
451
|
+
schema: {
|
|
452
|
+
params: ContentParamsSchema,
|
|
453
|
+
response: {
|
|
454
|
+
200: PreviewTokenResponseSchema,
|
|
455
|
+
401: ApiErrorSchema,
|
|
456
|
+
404: ApiErrorSchema,
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
}, async (request, reply) => {
|
|
460
|
+
const node = await adapter.getContentNode(request.params.id);
|
|
461
|
+
if (!node) {
|
|
462
|
+
return reply.code(404).send({ message: `No content found for id ${request.params.id}` });
|
|
463
|
+
}
|
|
464
|
+
const { token, expiresAt } = mintPreviewToken(node.id);
|
|
465
|
+
return {
|
|
466
|
+
token,
|
|
467
|
+
expiresAt,
|
|
468
|
+
previewUrl: buildPreviewUrl(token, node.id),
|
|
469
|
+
};
|
|
470
|
+
});
|
|
471
|
+
};
|