@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,477 @@
|
|
|
1
|
+
import { ApiErrorSchema, SetupCompleteRequestSchema, SetupCompleteResponseSchema, SetupStatusResponseSchema } from "@velora-cms/api-schemas";
|
|
2
|
+
import { clientDirFor } from "../scripts/generate-prisma-clients.js";
|
|
3
|
+
import { loadPrismaClient } from "../db/load-prisma-client.js";
|
|
4
|
+
import { buildConnectionString } from "../setup/build-connection-string.js";
|
|
5
|
+
import { provisionDatabase } from "../setup/provision-database.js";
|
|
6
|
+
import { persistDatabaseConfig } from "../setup/persist-env-config.js";
|
|
7
|
+
import { hashPassword } from "../auth/tokens.js";
|
|
8
|
+
import { listBundledTemplates } from "../setup/bundled-templates.js";
|
|
9
|
+
import { installStarterTemplate } from "../setup/install-starter.js";
|
|
10
|
+
import { SETUP_RATE_LIMIT, ipOnlyRateLimitConfig } from "../auth/route-rate-limits.js";
|
|
11
|
+
import { resolveEnvFilePath } from "../config/env-file-path.js";
|
|
12
|
+
import { dialectFromUrlScheme } from "../db/content-db.js";
|
|
13
|
+
import { applyCoreMigrations } from "../db/core-migrations/index.js";
|
|
14
|
+
import { ensurePrismaClientGenerated } from "../setup/ensure-prisma-client.js";
|
|
15
|
+
import { ADMIN_PROBE_TIMEOUT_MS, countAdminUsers, withTimeout } from "../boot/resolve-boot-mode.js";
|
|
16
|
+
// F6 (fix wave 1)/M3: named so the 409/503 message literals exist exactly
|
|
17
|
+
// once each, and tests assert against these constants instead of
|
|
18
|
+
// duplicating the string or matching a regex against it.
|
|
19
|
+
export const SETUP_ALREADY_COMPLETED_MESSAGE = "Setup has already been completed.";
|
|
20
|
+
export const SETUP_PROBE_FAILED_MESSAGE = "Could not verify whether setup has already been completed. Try again.";
|
|
21
|
+
// M2: the two pairing guards below constrain `database` (the request
|
|
22
|
+
// body), not DATABASE_URL (the environment) — they cannot, by themselves,
|
|
23
|
+
// prove process.env.DATABASE_URL is actually set. In production
|
|
24
|
+
// opts.databaseConfigured: true is passed by index.ts after
|
|
25
|
+
// resolveBootMode() (boot/resolve-boot-mode.ts) confirmed DATABASE_URL is
|
|
26
|
+
// set (the setup-admin-only path), and also by app.ts's own registration
|
|
27
|
+
// of this same route with { setupRequired: false, databaseConfigured:
|
|
28
|
+
// true } (the already-fully-configured, buildApp() path — DATABASE_URL is
|
|
29
|
+
// always set there too, or buildApp() itself would already have failed
|
|
30
|
+
// elsewhere). Harmless in both real callers because the 409
|
|
31
|
+
// already-completed guard fires first on that second path, but
|
|
32
|
+
// buildSetupOnlyApp() is exported and callable with
|
|
33
|
+
// { databaseConfigured: true } independently of either guarantee (tests
|
|
34
|
+
// do exactly this) — so this message exists for the case where neither
|
|
35
|
+
// production invariant holds.
|
|
36
|
+
export const SETUP_MISCONFIGURED_DATABASE_URL_MESSAGE = "Setup is misconfigured: the database is reported as configured, but no database URL is set.";
|
|
37
|
+
// Task 5 final follow-ups (S164 review), item 1: distinct from
|
|
38
|
+
// SETUP_ALREADY_COMPLETED_MESSAGE on purpose. A setup_lock row and its
|
|
39
|
+
// admin commit together (createFirstAdmin's own transaction, below), so a
|
|
40
|
+
// setup_lock row surviving with zero admins can only mean an admin was
|
|
41
|
+
// deleted after setup completed — the classic lost-password recovery.
|
|
42
|
+
// Without this distinct message that state answered the ordinary "already
|
|
43
|
+
// completed" 409 forever, with nothing naming setup_lock as the cause and
|
|
44
|
+
// no way back in. See createFirstAdmin's SetupLockWedgedError.
|
|
45
|
+
export const SETUP_LOCK_WEDGED_MESSAGE = "Setup cannot run: a setup_lock row exists but no admin account does. This only happens when an " +
|
|
46
|
+
"administrator account was deleted after setup had already completed. To recover, delete the single " +
|
|
47
|
+
"row (id = 1) from the setup_lock table, then re-run setup.";
|
|
48
|
+
/** Duck-typed on `.code`, same reasoning as
|
|
49
|
+
* marketplace/install-from-marketplace.ts's own isUniqueConstraintViolation:
|
|
50
|
+
* the client loaded here comes from a per-dialect generated module
|
|
51
|
+
* (db/load-prisma-client.ts), a different module instance than any
|
|
52
|
+
* statically imported `@prisma/client`, so `instanceof
|
|
53
|
+
* Prisma.PrismaClientKnownRequestError` would always be false across that
|
|
54
|
+
* boundary. `.code` is stable regardless of which client instance threw. */
|
|
55
|
+
function isUniqueConstraintViolation(error) {
|
|
56
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "P2002";
|
|
57
|
+
}
|
|
58
|
+
// Task 5 final follow-ups (S164 review), item 1: thrown by createFirstAdmin
|
|
59
|
+
// when it can prove the setup_lock sentinel is wedged (row present, zero
|
|
60
|
+
// admins) rather than just an ordinary concurrent loser. Caught at the
|
|
61
|
+
// route call site below and mapped to SETUP_LOCK_WEDGED_MESSAGE instead of
|
|
62
|
+
// SETUP_ALREADY_COMPLETED_MESSAGE. Not exported: nothing outside this
|
|
63
|
+
// module needs to name it, only catch it.
|
|
64
|
+
class SetupLockWedgedError extends Error {
|
|
65
|
+
}
|
|
66
|
+
// F3 (fix wave 2, S164 review): creates the first admin as one atomic
|
|
67
|
+
// unit, GUARANTEED atomic on every Tier 1 dialect — not just narrowed —
|
|
68
|
+
// unlike fix wave 1's count-and-create-in-one-$transaction, which this
|
|
69
|
+
// replaces.
|
|
70
|
+
//
|
|
71
|
+
// Fix wave 1's claim was wrong: a Prisma interactive transaction inherits
|
|
72
|
+
// the database's default isolation (READ COMMITTED on Postgres,
|
|
73
|
+
// REPEATABLE READ on MySQL), and a plain COUNT is a non-locking snapshot
|
|
74
|
+
// read with no phantom protection for a later INSERT in the same
|
|
75
|
+
// transaction. Reproduced live — two genuinely concurrent
|
|
76
|
+
// createFirstAdmin-shaped transactions, each on its own connection,
|
|
77
|
+
// BOTH committed a create on both postgresql and mysql, at gaps from 5ms
|
|
78
|
+
// to 200ms. Only SQLite hid this, because Prisma's single-connection pool
|
|
79
|
+
// serializes every write there regardless of isolation level — the one
|
|
80
|
+
// dialect fix wave 1's own test exercised. That test was a sequential
|
|
81
|
+
// stale-read scenario (any isolation level handles that); it was never
|
|
82
|
+
// evidence of atomicity under real concurrency.
|
|
83
|
+
//
|
|
84
|
+
// The fix: SetupLock (prisma/schema.prisma) is a fixed-PK singleton row.
|
|
85
|
+
// Every completion — admin-only AND setup-full, closing M-3 as well as F3
|
|
86
|
+
// since neither branches on mode anymore — inserts it unconditionally,
|
|
87
|
+
// inside the same transaction as the admin count/create. A PK insert is
|
|
88
|
+
// enforced by the storage engine itself: a second concurrent INSERT of
|
|
89
|
+
// the same key value blocks until the first inserting transaction
|
|
90
|
+
// resolves, then fails with a real unique-constraint violation (P2002) if
|
|
91
|
+
// it committed — on every Tier 1 dialect, regardless of isolation level.
|
|
92
|
+
// That is a fundamentally different guarantee than a COUNT snapshot read,
|
|
93
|
+
// which is why this closes the race a plain count-and-create could not.
|
|
94
|
+
//
|
|
95
|
+
// Task 5 final follow-ups (S164 review), item 3: this only holds for as
|
|
96
|
+
// long as the WINNER's transaction stays inside Prisma's default 5-second
|
|
97
|
+
// interactive-transaction timeout. Past that window the loser's blocked
|
|
98
|
+
// INSERT sees the winner's transaction time out and roll back rather than
|
|
99
|
+
// commit, so the loser's own statement resolves with no conflict at all —
|
|
100
|
+
// or, if the loser's OWN transaction is what times out while still
|
|
101
|
+
// blocked, Prisma raises P2028 instead of P2002. Neither is caught by
|
|
102
|
+
// isUniqueConstraintViolation below, so it is rethrown out of this
|
|
103
|
+
// function and — uncaught by the route handler beyond that — becomes an
|
|
104
|
+
// undeclared 500 rather than this function's 409. Still fail-closed (no
|
|
105
|
+
// second admin either way, since one of the two writes always loses), just
|
|
106
|
+
// a different status code once a transaction runs that long.
|
|
107
|
+
async function createFirstAdmin(client, admin) {
|
|
108
|
+
const data = {
|
|
109
|
+
email: admin.email,
|
|
110
|
+
name: admin.name,
|
|
111
|
+
passwordHash: await hashPassword(admin.password),
|
|
112
|
+
role: "admin",
|
|
113
|
+
};
|
|
114
|
+
try {
|
|
115
|
+
return await client.$transaction(async (tx) => {
|
|
116
|
+
const existingAdmins = await tx.user.count({ where: { role: "admin" } });
|
|
117
|
+
if (existingAdmins > 0)
|
|
118
|
+
return null;
|
|
119
|
+
// The loser of a genuine concurrent race throws P2002 here, caught
|
|
120
|
+
// below — this is what actually closes the window; the count above
|
|
121
|
+
// is only what makes an ALREADY-admin'd database (a pre-existing
|
|
122
|
+
// admin-only install predating this fix, which never got a
|
|
123
|
+
// SetupLock row) refuse without needing the sentinel at all.
|
|
124
|
+
await tx.setupLock.create({ data: { id: 1 } });
|
|
125
|
+
return tx.user.create({ data });
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
if (isUniqueConstraintViolation(err)) {
|
|
130
|
+
// Task 5 final follow-ups (S164 review), item 1: the transaction
|
|
131
|
+
// above just aborted on the storage engine's own P2002 — on Postgres
|
|
132
|
+
// in particular, an aborted transaction refuses every further
|
|
133
|
+
// statement until it rolls back, so there is no way to keep reading
|
|
134
|
+
// INSIDE it. This recount is therefore its own separate read against
|
|
135
|
+
// `client`, taken after that rollback has already happened.
|
|
136
|
+
//
|
|
137
|
+
// A count of zero here is provable evidence of the wedge: the
|
|
138
|
+
// sentinel and its admin always commit together (same transaction,
|
|
139
|
+
// above), so a P2002 on the sentinel with STILL zero admins means
|
|
140
|
+
// the row that P2002 collided with cannot have been created by a
|
|
141
|
+
// request racing this one — it can only be a sentinel left behind by
|
|
142
|
+
// an admin that existed and was later deleted. A non-zero count
|
|
143
|
+
// means an admin exists right now — either a genuine concurrent
|
|
144
|
+
// winner, or a pre-existing admin — the ordinary case.
|
|
145
|
+
//
|
|
146
|
+
// This has its own (narrower) race: a genuine concurrent winner
|
|
147
|
+
// could still be between its P2002-causing commit and this recount,
|
|
148
|
+
// in which case this recount could read 0 and misreport an ordinary
|
|
149
|
+
// race as a wedge. It cannot misreport in the other direction — an
|
|
150
|
+
// ordinary winner's commit is exactly what would make this count
|
|
151
|
+
// non-zero — so the mistake this leaves possible is a false wedge
|
|
152
|
+
// report, never a false "all clear," and either outcome still
|
|
153
|
+
// refuses with a 409 and creates no second admin.
|
|
154
|
+
const adminsNow = await client.user.count({ where: { role: "admin" } });
|
|
155
|
+
if (adminsNow === 0)
|
|
156
|
+
throw new SetupLockWedgedError();
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
throw err;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Registered by BOTH apps: buildApp() with setupRequired: false (a
|
|
163
|
+
// fully-configured instance answering "no, setup already happened"),
|
|
164
|
+
// buildSetupOnlyApp() with setupRequired: true — one shared route
|
|
165
|
+
// definition instead of two near-identical copies. Session 164 adds a
|
|
166
|
+
// third caller shape: buildSetupOnlyApp({ databaseConfigured: true }),
|
|
167
|
+
// the admin-only mode — still setupRequired: true (the wizard is still
|
|
168
|
+
// running), but with a database already injected and read-only.
|
|
169
|
+
export const setupRoutes = async (app, opts) => {
|
|
170
|
+
// F2 (fix wave 1): closes the setup-completion window for as long as
|
|
171
|
+
// THIS registered app instance keeps running, without waiting for a
|
|
172
|
+
// restart. In production index.ts builds exactly one app per boot
|
|
173
|
+
// (buildApp() or buildSetupOnlyApp(), never both), so this closure's
|
|
174
|
+
// lifetime IS the process's lifetime — "in-process latch." Scoped to
|
|
175
|
+
// this plugin registration rather than module scope so every
|
|
176
|
+
// independently built app (every test that calls buildSetupOnlyApp()/
|
|
177
|
+
// buildTestApp() gets its own fresh app) starts with its own latch too;
|
|
178
|
+
// a module-level flag would leak between unrelated app instances built
|
|
179
|
+
// in the same test process. Set the moment a completion actually
|
|
180
|
+
// succeeds, below, and checked at entry here — before the setupRequired
|
|
181
|
+
// check reads the boot-time flag opts.setupRequired never changes for
|
|
182
|
+
// the lifetime of this closure, which is exactly why a request-time
|
|
183
|
+
// re-check was needed at all (S164 final review of Task 4): without it,
|
|
184
|
+
// a first legitimate completion would not close this endpoint on its
|
|
185
|
+
// own — it would keep accepting unauthenticated admin-creation/database-
|
|
186
|
+
// provisioning requests until the next restart.
|
|
187
|
+
let setupCompleted = false;
|
|
188
|
+
// S162: an unauthenticated POST that provisions a database and creates
|
|
189
|
+
// the first admin. It 409s on an already-configured instance (S151), so
|
|
190
|
+
// the ceiling is about making the pre-configuration window uninteresting
|
|
191
|
+
// to probe, not about the configured steady state.
|
|
192
|
+
//
|
|
193
|
+
// ipOnlyRateLimitConfig() (I6, S162 final review; reshaped into a builder
|
|
194
|
+
// by Session 163's gate run), not the shared three-lane rateLimitConfig: this
|
|
195
|
+
// scope is also registered inside buildSetupOnlyApp() (setup-app.ts), the
|
|
196
|
+
// not-yet-configured boot path documented as never touching Prisma.
|
|
197
|
+
// rateLimitConfig imports auth/api-keys.ts -> db/prisma.ts, which has a
|
|
198
|
+
// top-level `await loadPrismaClient(...)` — importing it here would make
|
|
199
|
+
// that documented invariant false. Setup has no API keys or JWT sessions
|
|
200
|
+
// to lane anyway, so the plain IP-keyed config costs nothing behaviorally.
|
|
201
|
+
//
|
|
202
|
+
// Called here, not passed as a bare reference: it's a builder function now
|
|
203
|
+
// (route-rate-limits.ts), not a plain object, so the plugin receives the
|
|
204
|
+
// built options object — evaluated at this exact registration point, same
|
|
205
|
+
// read-timing as before the reshape.
|
|
206
|
+
await app.register(import("@fastify/rate-limit"), ipOnlyRateLimitConfig());
|
|
207
|
+
app.get("/api/setup/status", {
|
|
208
|
+
schema: {
|
|
209
|
+
response: {
|
|
210
|
+
200: SetupStatusResponseSchema,
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
}, async () => ({
|
|
214
|
+
setupRequired: opts.setupRequired,
|
|
215
|
+
// Only offered while setup is actually in progress — a
|
|
216
|
+
// fully-configured instance (setupRequired: false) has nothing left
|
|
217
|
+
// to pick a starter template for, so this route never bothers
|
|
218
|
+
// resolving the bundle on that path.
|
|
219
|
+
bundledTemplates: opts.setupRequired ? listBundledTemplates() : [],
|
|
220
|
+
databaseConfigured: opts.databaseConfigured,
|
|
221
|
+
}));
|
|
222
|
+
app.post("/api/setup/complete", {
|
|
223
|
+
schema: {
|
|
224
|
+
body: SetupCompleteRequestSchema,
|
|
225
|
+
response: {
|
|
226
|
+
200: SetupCompleteResponseSchema,
|
|
227
|
+
400: ApiErrorSchema,
|
|
228
|
+
409: ApiErrorSchema,
|
|
229
|
+
500: ApiErrorSchema,
|
|
230
|
+
503: ApiErrorSchema,
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
config: { rateLimit: { max: SETUP_RATE_LIMIT.max(), timeWindow: SETUP_RATE_LIMIT.timeWindow() } },
|
|
234
|
+
}, async (request, reply) => {
|
|
235
|
+
// buildApp() registers this route with setupRequired: false — on a
|
|
236
|
+
// configured instance, completing setup again would let any
|
|
237
|
+
// unauthenticated caller provision an attacker-supplied database,
|
|
238
|
+
// create their own admin in it, and REWRITE the real .env. Refuse
|
|
239
|
+
// before any provisioning side effect. setupCompleted (F2 above)
|
|
240
|
+
// answers the identical 409 once THIS app instance has already
|
|
241
|
+
// completed setup once, even though opts.setupRequired itself never
|
|
242
|
+
// changes.
|
|
243
|
+
if (!opts.setupRequired || setupCompleted) {
|
|
244
|
+
return reply.code(409).send({ message: SETUP_ALREADY_COMPLETED_MESSAGE });
|
|
245
|
+
}
|
|
246
|
+
const { admin, database, starter } = request.body;
|
|
247
|
+
// The pairing is enforced here, not by the schema, and in BOTH
|
|
248
|
+
// directions. Accepting-and-ignoring a selection would let an
|
|
249
|
+
// operator believe they had changed the database when the injected
|
|
250
|
+
// DATABASE_URL is what actually stands.
|
|
251
|
+
if (opts.databaseConfigured && database) {
|
|
252
|
+
return reply.code(400).send({
|
|
253
|
+
message: "The database is already configured by the environment and cannot be changed here. " +
|
|
254
|
+
"Remove the database selection from this request.",
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
if (!opts.databaseConfigured && !database) {
|
|
258
|
+
return reply.code(400).send({ message: "A database selection is required to complete setup." });
|
|
259
|
+
}
|
|
260
|
+
// Security fix (S164 final review of Task 4): opts.setupRequired /
|
|
261
|
+
// opts.databaseConfigured are frozen at BOOT — read once by
|
|
262
|
+
// index.ts's call into resolveBootMode() (boot/resolve-boot-mode.ts)
|
|
263
|
+
// and handed to whichever app got built. In admin-only mode this
|
|
264
|
+
// route stays registered, and therefore reachable, for as long as
|
|
265
|
+
// the process keeps running. Without this re-check, a first
|
|
266
|
+
// legitimate call that creates the admin would not close this
|
|
267
|
+
// endpoint: it would stay an unauthenticated "provision + create
|
|
268
|
+
// admin + rewrite DATABASE_URL" route until someone restarts the
|
|
269
|
+
// process. Re-probing here, at request time, closes that window
|
|
270
|
+
// immediately instead of on the next boot. Reuses countAdminUsers
|
|
271
|
+
// (boot/resolve-boot-mode.ts) — the same admin-existence question
|
|
272
|
+
// resolveBootMode already answers at boot, not a second
|
|
273
|
+
// implementation of it. F4 (fix wave 1): bounded with the same
|
|
274
|
+
// withTimeout/ADMIN_PROBE_TIMEOUT_MS resolveBootMode uses for its
|
|
275
|
+
// own boot-time probe — an unbounded await here would hang the
|
|
276
|
+
// REQUEST forever on a wedged database (M-1, fix wave 2: withTimeout
|
|
277
|
+
// does not cancel the underlying countAdminUsers() promise or
|
|
278
|
+
// disconnect its client early — it only races a timer against it, so
|
|
279
|
+
// the client is held open until that probe eventually settles on its
|
|
280
|
+
// own. What this bound actually buys is that the CALLER stops
|
|
281
|
+
// waiting and gets a named 503 instead of a hang; it is not a
|
|
282
|
+
// resource-leak fix).
|
|
283
|
+
//
|
|
284
|
+
// This only applies in admin-only mode (opts.databaseConfigured &&
|
|
285
|
+
// opts.setupRequired — setup-full has no configured database to
|
|
286
|
+
// probe, and setupRequired: false already returned 409 above). This
|
|
287
|
+
// pre-check is a cheap, non-atomic optimization — it lets an
|
|
288
|
+
// already-configured instance reject FAST, before running `prisma db
|
|
289
|
+
// push` or opening a real connection for the create below, but it
|
|
290
|
+
// does not by itself close the race between two concurrent
|
|
291
|
+
// requests. createFirstAdmin's own sentinel-backed transaction
|
|
292
|
+
// (F3, fix wave 2, above) is what actually closes that race; this
|
|
293
|
+
// check just keeps the common case cheap.
|
|
294
|
+
if (opts.databaseConfigured) {
|
|
295
|
+
let admins;
|
|
296
|
+
try {
|
|
297
|
+
admins = await withTimeout(countAdminUsers(), ADMIN_PROBE_TIMEOUT_MS);
|
|
298
|
+
}
|
|
299
|
+
catch (err) {
|
|
300
|
+
// FAIL CLOSED, matching resolveBootMode's own doctrine
|
|
301
|
+
// (boot/resolve-boot-mode.ts): a probe that rejects must never
|
|
302
|
+
// be read as "no admin yet" — that misreading is exactly the
|
|
303
|
+
// hole Task 4's review caught one level up, at boot. 503, not
|
|
304
|
+
// 400 or 500: this is neither a bad request nor a generic bug,
|
|
305
|
+
// it is the server unable to confirm whether setup is still
|
|
306
|
+
// open, and the operator should retry once the database answers.
|
|
307
|
+
app.log.error({ err }, "setup/complete: admin re-probe failed");
|
|
308
|
+
return reply.code(503).send({ message: SETUP_PROBE_FAILED_MESSAGE });
|
|
309
|
+
}
|
|
310
|
+
// Same "resolved but not a real count" doctrine as
|
|
311
|
+
// resolveBootMode: NaN, a negative number, or anything else that
|
|
312
|
+
// isn't a non-negative integer must refuse, not be treated as
|
|
313
|
+
// zero.
|
|
314
|
+
if (!Number.isInteger(admins) || admins < 0) {
|
|
315
|
+
app.log.error({ admins }, "setup/complete: admin re-probe resolved with a non-count value");
|
|
316
|
+
return reply.code(503).send({ message: SETUP_PROBE_FAILED_MESSAGE });
|
|
317
|
+
}
|
|
318
|
+
if (admins > 0) {
|
|
319
|
+
return reply.code(409).send({ message: SETUP_ALREADY_COMPLETED_MESSAGE });
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// In admin-only mode the URL comes from the environment, already
|
|
323
|
+
// normalised in index.ts. M2 (fix wave 1): the two pairing guards
|
|
324
|
+
// above make exactly one of these branches reachable IN PRODUCTION —
|
|
325
|
+
// `database` is defined whenever opts.databaseConfigured is false,
|
|
326
|
+
// and DATABASE_URL is guaranteed set whenever it's true, because
|
|
327
|
+
// that is what "databaseConfigured" means (see BootMode /
|
|
328
|
+
// resolveBootMode in boot/resolve-boot-mode.ts, and index.ts's call
|
|
329
|
+
// into buildSetupOnlyApp). But that guarantee lives in index.ts, not
|
|
330
|
+
// in this function's own inputs — buildSetupOnlyApp() is exported
|
|
331
|
+
// and callable with { databaseConfigured: true } independently of
|
|
332
|
+
// it (tests do exactly this), so a bare `!` here would trade a
|
|
333
|
+
// declared, tested 500 for an undeclared, untested one the moment
|
|
334
|
+
// that external guarantee doesn't hold. Checked explicitly instead.
|
|
335
|
+
let databaseUrl;
|
|
336
|
+
if (database) {
|
|
337
|
+
databaseUrl = buildConnectionString(database);
|
|
338
|
+
}
|
|
339
|
+
else if (process.env.DATABASE_URL) {
|
|
340
|
+
databaseUrl = process.env.DATABASE_URL;
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
return reply.code(500).send({ message: SETUP_MISCONFIGURED_DATABASE_URL_MESSAGE });
|
|
344
|
+
}
|
|
345
|
+
const dialect = database?.dialect ?? dialectFromUrlScheme(databaseUrl);
|
|
346
|
+
if (!dialect) {
|
|
347
|
+
return reply.code(400).send({
|
|
348
|
+
message: `The configured DATABASE_URL does not name a supported database (${databaseUrl.split(":")[0]}:).`,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
try {
|
|
352
|
+
// Runs on BOTH paths. `prisma db push` is idempotent, and an
|
|
353
|
+
// injected DATABASE_URL may point at a completely empty database —
|
|
354
|
+
// without this, the admin create below would fail on a missing
|
|
355
|
+
// table. S185: the Prisma client self-heal runs first — not
|
|
356
|
+
// because `db push --skip-generate` itself needs a generated
|
|
357
|
+
// client (it doesn't), but because the `loadPrismaClient(
|
|
358
|
+
// clientDirFor(dialect))` call a few lines below this block does —
|
|
359
|
+
// and the core-migration ledger is recorded in "fresh-push" mode
|
|
360
|
+
// right after — db push just created the CURRENT schema from
|
|
361
|
+
// scratch, so every step is already reflected and gets recorded
|
|
362
|
+
// without executing any SQL.
|
|
363
|
+
await ensurePrismaClientGenerated(dialect);
|
|
364
|
+
await provisionDatabase(dialect, databaseUrl);
|
|
365
|
+
await applyCoreMigrations({ dialect, databaseUrl, mode: "fresh-push" });
|
|
366
|
+
}
|
|
367
|
+
catch (err) {
|
|
368
|
+
return reply.code(400).send({ message: err instanceof Error ? err.message : "Failed to provision database" });
|
|
369
|
+
}
|
|
370
|
+
const client = await loadPrismaClient(clientDirFor(dialect), databaseUrl);
|
|
371
|
+
// The optional starter install (Session 151) needs both the created
|
|
372
|
+
// admin row's id (sample content's createdBy) and the still-connected
|
|
373
|
+
// client, so the disconnect moved from around the create alone to
|
|
374
|
+
// after the whole block. Without a starter this executes exactly the
|
|
375
|
+
// operations it always did — create, disconnect, persist — nothing
|
|
376
|
+
// content-db-shaped ever runs on the skip path.
|
|
377
|
+
let starterResult;
|
|
378
|
+
let adminUser = null;
|
|
379
|
+
// Task 5 final follow-ups (S164 review), item 1: set only when
|
|
380
|
+
// createFirstAdmin proved the setup_lock sentinel is wedged (see its
|
|
381
|
+
// own comment) — distinct from the ordinary `!adminUser` refusal
|
|
382
|
+
// below, which covers both "an admin already exists" and "an
|
|
383
|
+
// ordinary concurrent loser."
|
|
384
|
+
let wedged = false;
|
|
385
|
+
try {
|
|
386
|
+
// F3 (fix wave 2): unconditional now — see createFirstAdmin's own
|
|
387
|
+
// comment for why the sentinel-backed transaction applies to both
|
|
388
|
+
// modes, closing M-3 (setup-full vs. setup-full) as well as the
|
|
389
|
+
// original admin-only race.
|
|
390
|
+
try {
|
|
391
|
+
adminUser = await createFirstAdmin(client, admin);
|
|
392
|
+
}
|
|
393
|
+
catch (err) {
|
|
394
|
+
if (err instanceof SetupLockWedgedError) {
|
|
395
|
+
wedged = true;
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
throw err;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
// M-2 (fix wave 2): latched immediately after a real create
|
|
402
|
+
// succeeds — inside this try, before the starter install and
|
|
403
|
+
// before $disconnect() below. Fix wave 1 set this flag only after
|
|
404
|
+
// the whole try/finally returned, which left a window: if
|
|
405
|
+
// $disconnect() throws AFTER a successful create, the handler 500s
|
|
406
|
+
// with setupCompleted still false, reopening (for setup-full) the
|
|
407
|
+
// exact unauthenticated-completion window F2 exists to close. A
|
|
408
|
+
// starter failure below still can't unwind the admin account or
|
|
409
|
+
// the database config (installStarterTemplate never throws), so
|
|
410
|
+
// latching before it runs is safe.
|
|
411
|
+
if (adminUser) {
|
|
412
|
+
setupCompleted = true;
|
|
413
|
+
}
|
|
414
|
+
if (adminUser && starter) {
|
|
415
|
+
// Never throws — a starter failure is reported in the response,
|
|
416
|
+
// it must not unwind the admin account or the database config.
|
|
417
|
+
starterResult = await installStarterTemplate({
|
|
418
|
+
prisma: client,
|
|
419
|
+
databaseUrl,
|
|
420
|
+
adminUserId: adminUser.id,
|
|
421
|
+
starter,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
finally {
|
|
426
|
+
await client.$disconnect();
|
|
427
|
+
}
|
|
428
|
+
if (wedged) {
|
|
429
|
+
// Task 5 final follow-ups (S164 review), item 1: named and logged
|
|
430
|
+
// so an operator locked out by a deleted admin has both a
|
|
431
|
+
// diagnosable response and a server-side record of when/why —
|
|
432
|
+
// unlike the silent, indistinguishable 409 this replaces.
|
|
433
|
+
app.log.error({ attemptedEmail: admin.email }, "setup/complete: setup_lock sentinel exists with zero admins — an admin was likely deleted after setup completed");
|
|
434
|
+
return reply.code(409).send({ message: SETUP_LOCK_WEDGED_MESSAGE });
|
|
435
|
+
}
|
|
436
|
+
if (!adminUser) {
|
|
437
|
+
// F3: the atomic transaction's own count/sentinel saw an admin
|
|
438
|
+
// that did not exist when the cheap pre-check ran above — another
|
|
439
|
+
// request, in this same process, won the race and created it
|
|
440
|
+
// first. Same 409 shape as every other "setup already completed"
|
|
441
|
+
// answer.
|
|
442
|
+
return reply.code(409).send({ message: SETUP_ALREADY_COMPLETED_MESSAGE });
|
|
443
|
+
}
|
|
444
|
+
// Only the setup-full path has anything to persist. In admin-only
|
|
445
|
+
// mode DATABASE_URL was already set at boot — by the environment or
|
|
446
|
+
// by a previous run's env file — so there is nothing new to write,
|
|
447
|
+
// and writing would risk contradicting what the next restart is
|
|
448
|
+
// injected with. See BootMode / resolveBootMode in
|
|
449
|
+
// boot/resolve-boot-mode.ts.
|
|
450
|
+
let configWarning;
|
|
451
|
+
if (!opts.databaseConfigured) {
|
|
452
|
+
try {
|
|
453
|
+
persistDatabaseConfig(databaseUrl);
|
|
454
|
+
}
|
|
455
|
+
catch (err) {
|
|
456
|
+
// Never unwind. The admin row and any starter install have
|
|
457
|
+
// already succeeded, and throwing here would lose them while
|
|
458
|
+
// leaving the database provisioned — the same reasoning the
|
|
459
|
+
// starter failure path uses a few lines above. Report it
|
|
460
|
+
// instead, naming the resolved path, so the operator can set
|
|
461
|
+
// DATABASE_URL themselves and restart.
|
|
462
|
+
configWarning =
|
|
463
|
+
`Setup finished, but the configuration could not be saved to ${resolveEnvFilePath()}: ` +
|
|
464
|
+
`${err instanceof Error ? err.message : String(err)}. ` +
|
|
465
|
+
"Set DATABASE_URL in the environment before restarting, or this instance will run setup again.";
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
// `starter` and `configWarning` stay ABSENT (not null) when there is
|
|
469
|
+
// nothing to report, keeping the ordinary response byte-identical to
|
|
470
|
+
// the pre-Session-164 shape.
|
|
471
|
+
return {
|
|
472
|
+
success: true,
|
|
473
|
+
...(starterResult ? { starter: starterResult } : {}),
|
|
474
|
+
...(configWarning ? { configWarning } : {}),
|
|
475
|
+
};
|
|
476
|
+
});
|
|
477
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { SnapshotService } from "@velora-cms/content-service";
|
|
3
|
+
import { ApiErrorSchema, CreateSnapshotRequestSchema, RestoreSnapshotResponseSchema, SnapshotListQuerySchema, SnapshotListResponseSchema, SnapshotResponseSchema, } from "@velora-cms/api-schemas";
|
|
4
|
+
import { requireAdmin } from "../auth/require-auth.js";
|
|
5
|
+
const SnapshotParamsSchema = z.object({ id: z.string().uuid() });
|
|
6
|
+
// Everything here is admin-only: a snapshot restore repoints every content
|
|
7
|
+
// node's published pointer at once — system-level blast radius, same
|
|
8
|
+
// gating tier as user management.
|
|
9
|
+
export const snapshotRoutes = async (app, opts) => {
|
|
10
|
+
const snapshotService = new SnapshotService(opts.adapter);
|
|
11
|
+
app.post("/api/snapshots", {
|
|
12
|
+
onRequest: requireAdmin,
|
|
13
|
+
schema: {
|
|
14
|
+
body: CreateSnapshotRequestSchema,
|
|
15
|
+
response: {
|
|
16
|
+
201: SnapshotResponseSchema,
|
|
17
|
+
401: ApiErrorSchema,
|
|
18
|
+
403: ApiErrorSchema,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
}, async (request, reply) => {
|
|
22
|
+
const { items: _items, ...meta } = await snapshotService.createSnapshot(request.body.label, request.user.sub, request.body.description ?? null);
|
|
23
|
+
return reply.code(201).send({ snapshot: meta });
|
|
24
|
+
});
|
|
25
|
+
app.get("/api/snapshots", {
|
|
26
|
+
onRequest: requireAdmin,
|
|
27
|
+
schema: {
|
|
28
|
+
querystring: SnapshotListQuerySchema,
|
|
29
|
+
response: {
|
|
30
|
+
200: SnapshotListResponseSchema,
|
|
31
|
+
401: ApiErrorSchema,
|
|
32
|
+
403: ApiErrorSchema,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
}, async (request) => {
|
|
36
|
+
const { cursor, limit } = request.query;
|
|
37
|
+
const items = await snapshotService.listSnapshots({ cursor, limit });
|
|
38
|
+
const nextCursor = items.length === limit ? (items[items.length - 1]?.id ?? null) : null;
|
|
39
|
+
return { items, nextCursor };
|
|
40
|
+
});
|
|
41
|
+
app.post("/api/snapshots/:id/restore", {
|
|
42
|
+
onRequest: requireAdmin,
|
|
43
|
+
schema: {
|
|
44
|
+
params: SnapshotParamsSchema,
|
|
45
|
+
response: {
|
|
46
|
+
200: RestoreSnapshotResponseSchema,
|
|
47
|
+
401: ApiErrorSchema,
|
|
48
|
+
403: ApiErrorSchema,
|
|
49
|
+
404: ApiErrorSchema,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
}, async (request, reply) => {
|
|
53
|
+
try {
|
|
54
|
+
const restored = await snapshotService.restoreSnapshot(request.params.id);
|
|
55
|
+
const { items, ...meta } = restored;
|
|
56
|
+
return { snapshot: meta, restoredItems: items.length };
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (error instanceof Error && /no snapshot found/i.test(error.message)) {
|
|
60
|
+
return reply.code(404).send({ message: error.message });
|
|
61
|
+
}
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { ApiErrorSchema, CreateTemplateRequestSchema, CreateTemplateResponseSchema, TemplateListQuerySchema, TemplateListResponseSchema, } from "@velora-cms/api-schemas";
|
|
3
|
+
import { requireAuth } from "../auth/require-auth.js";
|
|
4
|
+
// Mirrors document-types.ts's shape exactly. Templates are genuinely inert
|
|
5
|
+
// data at this point — no rendering engine consumes them yet (per the
|
|
6
|
+
// Document Types tabs/preview/templates plan revision) — this just lets
|
|
7
|
+
// a document type record which named template(s) exist for it.
|
|
8
|
+
export const templateRoutes = async (app, opts) => {
|
|
9
|
+
const { adapter } = opts;
|
|
10
|
+
app.post("/api/templates", {
|
|
11
|
+
onRequest: requireAuth,
|
|
12
|
+
schema: {
|
|
13
|
+
body: CreateTemplateRequestSchema,
|
|
14
|
+
response: {
|
|
15
|
+
201: CreateTemplateResponseSchema,
|
|
16
|
+
401: ApiErrorSchema,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
}, async (request, reply) => {
|
|
20
|
+
const template = { id: randomUUID(), ...request.body };
|
|
21
|
+
await adapter.createTemplate(template);
|
|
22
|
+
return reply.code(201).send({ template });
|
|
23
|
+
});
|
|
24
|
+
app.get("/api/templates", {
|
|
25
|
+
schema: {
|
|
26
|
+
querystring: TemplateListQuerySchema,
|
|
27
|
+
response: {
|
|
28
|
+
200: TemplateListResponseSchema,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
}, async (request) => {
|
|
32
|
+
const { cursor, limit, documentTypeId } = request.query;
|
|
33
|
+
const items = await adapter.listTemplates({ cursor, limit, documentTypeId });
|
|
34
|
+
const nextCursor = items.length === limit ? (items[items.length - 1]?.id ?? null) : null;
|
|
35
|
+
return { items, nextCursor };
|
|
36
|
+
});
|
|
37
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ThemeListResponseSchema } from "@velora-cms/api-schemas";
|
|
2
|
+
import { prisma } from "../db/prisma.js";
|
|
3
|
+
// Read-only, no auth (the GET /api/plugins posture): theme tokens are
|
|
4
|
+
// cosmetic metadata. Rows are the source of WHICH themes are installed
|
|
5
|
+
// and enabled; the attached manifest is the source of the tokens — a
|
|
6
|
+
// row whose manifest isn't attached in this process (mid-install crash,
|
|
7
|
+
// stale row) is skipped rather than served without tokens.
|
|
8
|
+
//
|
|
9
|
+
// No pagination: installed themes are a handful, never a growing content
|
|
10
|
+
// or media list — the cursor rule (api-design.md) targets those, not this.
|
|
11
|
+
export const themeRoutes = async (app) => {
|
|
12
|
+
app.get("/api/themes", { schema: { response: { 200: ThemeListResponseSchema } } }, async () => {
|
|
13
|
+
const rows = await prisma.pluginRegistry.findMany({
|
|
14
|
+
where: { enabled: true, type: "theme" },
|
|
15
|
+
orderBy: { id: "asc" },
|
|
16
|
+
});
|
|
17
|
+
const items = [];
|
|
18
|
+
for (const row of rows) {
|
|
19
|
+
const manifest = app.pluginRegistry.getManifest(row.pluginId);
|
|
20
|
+
if (!manifest || manifest.type !== "theme")
|
|
21
|
+
continue;
|
|
22
|
+
const tokens = manifest.contributions.themeTokens;
|
|
23
|
+
if (!tokens)
|
|
24
|
+
continue;
|
|
25
|
+
// ThemeTokenOverrides interface vs Record<string, unknown> assignability limitation
|
|
26
|
+
items.push({ pluginId: row.pluginId, name: row.name, author: manifest.author, tokens: tokens });
|
|
27
|
+
}
|
|
28
|
+
return { items };
|
|
29
|
+
});
|
|
30
|
+
};
|