@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,29 @@
|
|
|
1
|
+
import type { PrismaClient } from "@prisma/client";
|
|
2
|
+
import type { DatabaseAdapter } from "@velora-cms/db-adapter";
|
|
3
|
+
import { type PluginRegistryService } from "./plugin-registry-service.js";
|
|
4
|
+
export declare class BundleMemberBlockedError extends Error {
|
|
5
|
+
constructor(memberPluginId: string, dependentPluginId: string);
|
|
6
|
+
}
|
|
7
|
+
export declare class BundleUninstallRequiresConfirmationError extends Error {
|
|
8
|
+
readonly memberSnapshots: Array<{
|
|
9
|
+
pluginId: string;
|
|
10
|
+
snapshotGroupId: string;
|
|
11
|
+
}>;
|
|
12
|
+
constructor(bundlePluginId: string, memberSnapshots: Array<{
|
|
13
|
+
pluginId: string;
|
|
14
|
+
snapshotGroupId: string;
|
|
15
|
+
}>);
|
|
16
|
+
}
|
|
17
|
+
export interface UninstallBundleDeps {
|
|
18
|
+
prisma: PrismaClient;
|
|
19
|
+
registry: PluginRegistryService;
|
|
20
|
+
adapter: DatabaseAdapter;
|
|
21
|
+
}
|
|
22
|
+
export declare function uninstallBundle(deps: UninstallBundleDeps, bundlePluginId: string, options: {
|
|
23
|
+
confirmStructuredDataCleanup: boolean;
|
|
24
|
+
}): Promise<{
|
|
25
|
+
memberSnapshots: Array<{
|
|
26
|
+
pluginId: string;
|
|
27
|
+
snapshotGroupId: string;
|
|
28
|
+
}>;
|
|
29
|
+
}>;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { DocumentTypeService } from "@velora-cms/content-service";
|
|
2
|
+
import { removePluginPackages } from "./plugin-package-store.js";
|
|
3
|
+
import { StructuredDataCleanupRequiresConfirmationError, } from "./plugin-registry-service.js";
|
|
4
|
+
// S152's bundle-uninstall cascade. Lives ABOVE PluginRegistryService —
|
|
5
|
+
// uninstall() stays single-plugin — at the same layer the DELETE route
|
|
6
|
+
// already orchestrates its per-plugin cross-cutting cleanup (package
|
|
7
|
+
// store, manifest detach, license rows), because the cascade is exactly
|
|
8
|
+
// that orchestration repeated per owned member.
|
|
9
|
+
//
|
|
10
|
+
// Order (the S88 check-first discipline, cross-plugin):
|
|
11
|
+
// 1. preflight EVERYTHING (doc-type deletability, outside dependents)
|
|
12
|
+
// 2. aggregate the structured-data confirmation gate (snapshots taken,
|
|
13
|
+
// nothing destroyed — the S87 safe-retry property, kept)
|
|
14
|
+
// 3. only then cascade: owned members in reverse install order, bundle
|
|
15
|
+
// row last.
|
|
16
|
+
export class BundleMemberBlockedError extends Error {
|
|
17
|
+
constructor(memberPluginId, dependentPluginId) {
|
|
18
|
+
super(`Cannot uninstall this bundle: its member "${memberPluginId}" is a dependency of "${dependentPluginId}", which is not part of the bundle — uninstall "${dependentPluginId}" first`);
|
|
19
|
+
this.name = "BundleMemberBlockedError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export class BundleUninstallRequiresConfirmationError extends Error {
|
|
23
|
+
memberSnapshots;
|
|
24
|
+
constructor(bundlePluginId, memberSnapshots) {
|
|
25
|
+
super(`Uninstalling bundle "${bundlePluginId}" will drop structured data for: ${memberSnapshots
|
|
26
|
+
.map((s) => `"${s.pluginId}"`)
|
|
27
|
+
.join(", ")}. Snapshots have been taken; retry with confirmStructuredDataCleanup: true to proceed`);
|
|
28
|
+
this.memberSnapshots = memberSnapshots;
|
|
29
|
+
this.name = "BundleUninstallRequiresConfirmationError";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export async function uninstallBundle(deps, bundlePluginId, options) {
|
|
33
|
+
const { prisma, registry, adapter } = deps;
|
|
34
|
+
// Owned members, in install order. Rows whose member registry row is
|
|
35
|
+
// gone (individually uninstalled since the bundle install — the
|
|
36
|
+
// recorded tolerated case) contribute no work; their stale ownership
|
|
37
|
+
// rows are swept at the end.
|
|
38
|
+
const ownershipRows = await prisma.pluginBundleMember.findMany({
|
|
39
|
+
where: { bundlePluginId },
|
|
40
|
+
orderBy: { position: "asc" },
|
|
41
|
+
});
|
|
42
|
+
const ownedIds = ownershipRows.map((row) => row.memberPluginId);
|
|
43
|
+
const memberRows = await prisma.pluginRegistry.findMany({ where: { pluginId: { in: ownedIds } } });
|
|
44
|
+
const existingMemberIds = new Set(memberRows.map((row) => row.pluginId));
|
|
45
|
+
const removalIds = new Set([bundlePluginId, ...existingMemberIds]);
|
|
46
|
+
// Preflight 1: every tracked document type of every plugin in the
|
|
47
|
+
// removal set must be deletable BEFORE anything is removed — the same
|
|
48
|
+
// guard uninstall() runs per plugin, hoisted so a blocked member is
|
|
49
|
+
// found while the whole set is still intact. Throws
|
|
50
|
+
// DocumentTypeInUseError (the route already maps it to 409).
|
|
51
|
+
const documentTypeService = new DocumentTypeService(adapter);
|
|
52
|
+
const trackedDocTypes = await prisma.pluginDocumentType.findMany({
|
|
53
|
+
where: { pluginId: { in: [...removalIds] } },
|
|
54
|
+
});
|
|
55
|
+
for (const tracked of trackedDocTypes) {
|
|
56
|
+
await documentTypeService.assertDocumentTypeDeletable(tracked.documentTypeId);
|
|
57
|
+
}
|
|
58
|
+
// Preflight 2: no plugin OUTSIDE the removal set may depend on an owned
|
|
59
|
+
// member (uninstall() itself has no dependents guard — without this, a
|
|
60
|
+
// cascade would silently break a dependent that had every right to rely
|
|
61
|
+
// on the "installed AND enabled" check that admitted it). Attached
|
|
62
|
+
// manifests are the source (complete on a booted server since S122); a
|
|
63
|
+
// row with no attached manifest cannot be checked and is skipped —
|
|
64
|
+
// consistent with uninstall()'s own missing-manifest tolerance.
|
|
65
|
+
const allRows = await prisma.pluginRegistry.findMany();
|
|
66
|
+
for (const row of allRows) {
|
|
67
|
+
if (removalIds.has(row.pluginId))
|
|
68
|
+
continue;
|
|
69
|
+
const manifest = registry.getManifest(row.pluginId);
|
|
70
|
+
const dependencies = manifest?.pluginDependencies ?? [];
|
|
71
|
+
const blocked = dependencies.find((dependency) => existingMemberIds.has(dependency));
|
|
72
|
+
if (blocked) {
|
|
73
|
+
throw new BundleMemberBlockedError(blocked, row.pluginId);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Preflight 3: the aggregated structured-data gate. Calling uninstall()
|
|
77
|
+
// UNCONFIRMED on a structured plugin snapshots it and then throws
|
|
78
|
+
// having changed nothing else (S87's ordering) — so harvesting each
|
|
79
|
+
// structured plugin's snapshotGroupId this way is non-destructive by
|
|
80
|
+
// construction. Simple-storage plugins are never dry-run (an
|
|
81
|
+
// unconfirmed uninstall() would really uninstall them).
|
|
82
|
+
if (!options.confirmStructuredDataCleanup) {
|
|
83
|
+
const structuredIds = [
|
|
84
|
+
...memberRows.filter((row) => row.storageType === "structured").map((row) => row.pluginId),
|
|
85
|
+
];
|
|
86
|
+
const bundleRow = await prisma.pluginRegistry.findUnique({ where: { pluginId: bundlePluginId } });
|
|
87
|
+
if (bundleRow?.storageType === "structured") {
|
|
88
|
+
structuredIds.push(bundlePluginId);
|
|
89
|
+
}
|
|
90
|
+
if (structuredIds.length > 0) {
|
|
91
|
+
const memberSnapshots = [];
|
|
92
|
+
for (const structuredId of structuredIds) {
|
|
93
|
+
try {
|
|
94
|
+
await registry.uninstall(structuredId, { confirmStructuredDataCleanup: false });
|
|
95
|
+
throw new Error(`uninstall("${structuredId}") completed without the structured-data confirmation gate firing — refusing to continue a bundle cascade on inconsistent state`);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (error instanceof StructuredDataCleanupRequiresConfirmationError) {
|
|
99
|
+
memberSnapshots.push({ pluginId: structuredId, snapshotGroupId: error.snapshotGroupId });
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
throw new BundleUninstallRequiresConfirmationError(bundlePluginId, memberSnapshots);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Cascade: owned members in REVERSE install order, each with the same
|
|
110
|
+
// route-level cleanup a single-plugin uninstall gets, deleting its
|
|
111
|
+
// ownership row as it completes so a mid-cascade failure leaves an
|
|
112
|
+
// honest picture for a retry. Bundle row last.
|
|
113
|
+
//
|
|
114
|
+
// Task 6: each confirmed uninstall() call now hands back the fresh
|
|
115
|
+
// snapshotGroupId it minted (null for a simple-storage member) — collect
|
|
116
|
+
// only the non-null ones here, same shape as the aggregated 409 above,
|
|
117
|
+
// so a confirmed bundle uninstall can report exactly which structured
|
|
118
|
+
// members got a fresh snapshot.
|
|
119
|
+
const memberSnapshots = [];
|
|
120
|
+
const orderedForRemoval = [...ownershipRows].reverse();
|
|
121
|
+
for (const ownership of orderedForRemoval) {
|
|
122
|
+
if (existingMemberIds.has(ownership.memberPluginId)) {
|
|
123
|
+
const { snapshotGroupId } = await registry.uninstall(ownership.memberPluginId, {
|
|
124
|
+
confirmStructuredDataCleanup: options.confirmStructuredDataCleanup,
|
|
125
|
+
});
|
|
126
|
+
if (snapshotGroupId) {
|
|
127
|
+
memberSnapshots.push({ pluginId: ownership.memberPluginId, snapshotGroupId });
|
|
128
|
+
}
|
|
129
|
+
removePluginPackages(ownership.memberPluginId);
|
|
130
|
+
registry.detachManifest(ownership.memberPluginId);
|
|
131
|
+
await prisma.pluginLicense.deleteMany({ where: { pluginId: ownership.memberPluginId } });
|
|
132
|
+
}
|
|
133
|
+
await prisma.pluginBundleMember.deleteMany({
|
|
134
|
+
where: { bundlePluginId, memberPluginId: ownership.memberPluginId },
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
const { snapshotGroupId: bundleSnapshotGroupId } = await registry.uninstall(bundlePluginId, {
|
|
138
|
+
confirmStructuredDataCleanup: options.confirmStructuredDataCleanup,
|
|
139
|
+
});
|
|
140
|
+
if (bundleSnapshotGroupId) {
|
|
141
|
+
memberSnapshots.push({ pluginId: bundlePluginId, snapshotGroupId: bundleSnapshotGroupId });
|
|
142
|
+
}
|
|
143
|
+
removePluginPackages(bundlePluginId);
|
|
144
|
+
registry.detachManifest(bundlePluginId);
|
|
145
|
+
await prisma.pluginLicense.deleteMany({ where: { pluginId: bundlePluginId } });
|
|
146
|
+
// Belt-and-braces: no rows should remain, but a row for a member that
|
|
147
|
+
// vanished mid-flight must not survive the bundle.
|
|
148
|
+
await prisma.pluginBundleMember.deleteMany({ where: { bundlePluginId } });
|
|
149
|
+
return { memberSnapshots };
|
|
150
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { TrustedKey, VerifyResult } from "@velora-cms/plugin-package";
|
|
2
|
+
/** The detached sidecar convention: <artifact>.tar.gz.sig beside the
|
|
3
|
+
* artifact. Detached rather than embedded because the signature is over the
|
|
4
|
+
* tarball's own hash and therefore cannot live inside it. */
|
|
5
|
+
export declare function resolveSidecarPath(tarballPath: string): string;
|
|
6
|
+
export declare function verifyPackage(tarballPath: string, sidecarPath: string | null, trustedKeys: TrustedKey[]): Promise<VerifyResult>;
|
|
7
|
+
/**
|
|
8
|
+
* The post-extraction half of verification. The envelope's pluginId and
|
|
9
|
+
* version can only be compared against the package's own manifest.json
|
|
10
|
+
* AFTER extraction, so this runs separately from verifyPackage and can
|
|
11
|
+
* DOWNGRADE a verified result.
|
|
12
|
+
*
|
|
13
|
+
* The envelope comes off the `verified` result itself (the one
|
|
14
|
+
* `verifyEnvelope` already authenticated), never from a second,
|
|
15
|
+
* unauthenticated read of the sidecar file — that would let a mismatched
|
|
16
|
+
* re-read silently replace the identity that was actually verified. If a
|
|
17
|
+
* `verified` result somehow carries no envelope, this DOWNGRADES rather
|
|
18
|
+
* than passing it through: a trust decision must never fail open by
|
|
19
|
+
* skipping the one check it exists to make.
|
|
20
|
+
*
|
|
21
|
+
* A cryptographically valid signature over the wrong package's identity
|
|
22
|
+
* must never yield "verified" — that is precisely the mistake a half-built
|
|
23
|
+
* approval flow could introduce.
|
|
24
|
+
*/
|
|
25
|
+
export declare function checkPackageIdentity(result: VerifyResult, pluginId: string, version: string): VerifyResult;
|
|
26
|
+
/** One place that decides how each outcome is surfaced. An unsigned package
|
|
27
|
+
* is silent — velora-dev and private agency plugins are unsigned by design
|
|
28
|
+
* and must not nag. A package that CLAIMS a signature and fails is loud and
|
|
29
|
+
* names the reason. */
|
|
30
|
+
export declare function describeVerifyResult(result: VerifyResult, tarballPath: string): void;
|
|
31
|
+
/** The two trust tiers a registry row can carry. Narrower than `string` so
|
|
32
|
+
* `PluginRegistryService.install`, `registerLocalPlugin`, and
|
|
33
|
+
* `installBuiltPackage` can't silently accept an arbitrary tier value. */
|
|
34
|
+
export type TrustTier = "verified" | "unverified";
|
|
35
|
+
/** The tier a verdict maps to. Only a clean "verified" earns the tier. */
|
|
36
|
+
export declare function trustTierFor(result: VerifyResult): TrustTier;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { hashTarball, verifyEnvelope } from "@velora-cms/plugin-package";
|
|
3
|
+
// Install-time signature verification (Session 127).
|
|
4
|
+
//
|
|
5
|
+
// NEVER throws, and NEVER blocks an install. Per the Trust model,
|
|
6
|
+
// sideloading is a requirement rather than a loophole: an unverifiable
|
|
7
|
+
// package still installs, as "unverified", with a loud warning. Only
|
|
8
|
+
// extraction can fail an install, because an archive that violates a
|
|
9
|
+
// hardening rule is not a package at all.
|
|
10
|
+
const LOG_PREFIX = "[plugin-verify]";
|
|
11
|
+
/** A generous ceiling for an envelope plus a base64 Ed25519 signature — a
|
|
12
|
+
* few KB at most. The tarball itself is streamed rather than buffered (see
|
|
13
|
+
* hashTarball), with its own size bound arriving later from the extractor's
|
|
14
|
+
* MAX_TOTAL_BYTES; the sidecar, by contrast, is read fully into memory
|
|
15
|
+
* below, so an attacker-supplied sidecar needs its own ceiling — one that
|
|
16
|
+
* had none before this, so an oversized file is rejected before it is ever
|
|
17
|
+
* read into memory. */
|
|
18
|
+
const MAX_SIDECAR_BYTES = 8192;
|
|
19
|
+
/** The detached sidecar convention: <artifact>.tar.gz.sig beside the
|
|
20
|
+
* artifact. Detached rather than embedded because the signature is over the
|
|
21
|
+
* tarball's own hash and therefore cannot live inside it. */
|
|
22
|
+
export function resolveSidecarPath(tarballPath) {
|
|
23
|
+
return `${tarballPath}.sig`;
|
|
24
|
+
}
|
|
25
|
+
export async function verifyPackage(tarballPath, sidecarPath, trustedKeys) {
|
|
26
|
+
if (!sidecarPath || !existsSync(sidecarPath)) {
|
|
27
|
+
return { status: "unsigned" };
|
|
28
|
+
}
|
|
29
|
+
let sidecar;
|
|
30
|
+
try {
|
|
31
|
+
// statSync lives inside this try, not before it, even though it only
|
|
32
|
+
// "checks a size": this module never throws, and a file that passed
|
|
33
|
+
// existsSync a moment ago can still fail to stat — deleted or replaced
|
|
34
|
+
// in the gap, or (on Windows) held open exclusively by whatever wrote
|
|
35
|
+
// it (EBUSY/EPERM). Hoisting it back above this try would let that race
|
|
36
|
+
// escape verifyPackage and abort an install that should instead proceed
|
|
37
|
+
// as "unverified".
|
|
38
|
+
if (statSync(sidecarPath).size > MAX_SIDECAR_BYTES) {
|
|
39
|
+
return {
|
|
40
|
+
status: "failed",
|
|
41
|
+
reason: "malformed-sidecar",
|
|
42
|
+
message: `${sidecarPath} exceeds the ${MAX_SIDECAR_BYTES}-byte sidecar size ceiling`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
sidecar = JSON.parse(readFileSync(sidecarPath, "utf8"));
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
return {
|
|
49
|
+
status: "failed",
|
|
50
|
+
reason: "malformed-sidecar",
|
|
51
|
+
message: `${sidecarPath} could not be read as a signature sidecar: ${error instanceof Error ? error.message : String(error)}`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
let sha256;
|
|
55
|
+
try {
|
|
56
|
+
sha256 = await hashTarball(tarballPath);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
return {
|
|
60
|
+
status: "failed",
|
|
61
|
+
reason: "artifact-unreadable",
|
|
62
|
+
message: `${tarballPath} could not be read: ${error instanceof Error ? error.message : String(error)}`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return verifyEnvelope(sidecar, sha256, trustedKeys);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The post-extraction half of verification. The envelope's pluginId and
|
|
69
|
+
* version can only be compared against the package's own manifest.json
|
|
70
|
+
* AFTER extraction, so this runs separately from verifyPackage and can
|
|
71
|
+
* DOWNGRADE a verified result.
|
|
72
|
+
*
|
|
73
|
+
* The envelope comes off the `verified` result itself (the one
|
|
74
|
+
* `verifyEnvelope` already authenticated), never from a second,
|
|
75
|
+
* unauthenticated read of the sidecar file — that would let a mismatched
|
|
76
|
+
* re-read silently replace the identity that was actually verified. If a
|
|
77
|
+
* `verified` result somehow carries no envelope, this DOWNGRADES rather
|
|
78
|
+
* than passing it through: a trust decision must never fail open by
|
|
79
|
+
* skipping the one check it exists to make.
|
|
80
|
+
*
|
|
81
|
+
* A cryptographically valid signature over the wrong package's identity
|
|
82
|
+
* must never yield "verified" — that is precisely the mistake a half-built
|
|
83
|
+
* approval flow could introduce.
|
|
84
|
+
*/
|
|
85
|
+
export function checkPackageIdentity(result, pluginId, version) {
|
|
86
|
+
if (result.status !== "verified")
|
|
87
|
+
return result;
|
|
88
|
+
const { envelope } = result;
|
|
89
|
+
if (!envelope) {
|
|
90
|
+
return {
|
|
91
|
+
status: "failed",
|
|
92
|
+
reason: "identity-mismatch",
|
|
93
|
+
message: "verified result carried no envelope to check identity against — refusing to trust it",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (envelope.pluginId === pluginId && envelope.version === version)
|
|
97
|
+
return result;
|
|
98
|
+
return {
|
|
99
|
+
status: "failed",
|
|
100
|
+
reason: "identity-mismatch",
|
|
101
|
+
message: `signature covers ${envelope.pluginId}@${envelope.version} but the package contains ${pluginId}@${version}`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/** One place that decides how each outcome is surfaced. An unsigned package
|
|
105
|
+
* is silent — velora-dev and private agency plugins are unsigned by design
|
|
106
|
+
* and must not nag. A package that CLAIMS a signature and fails is loud and
|
|
107
|
+
* names the reason. */
|
|
108
|
+
export function describeVerifyResult(result, tarballPath) {
|
|
109
|
+
if (result.status === "verified") {
|
|
110
|
+
console.log(`${LOG_PREFIX} verified ${tarballPath} against trusted key ${result.keyId}`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (result.status === "failed") {
|
|
114
|
+
console.warn(`${LOG_PREFIX} SIGNATURE VERIFICATION FAILED for ${tarballPath} (${result.reason}): ${result.message}`);
|
|
115
|
+
console.warn(`${LOG_PREFIX} installing as "unverified" — the package is NOT blocked, but it is NOT trusted`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** The tier a verdict maps to. Only a clean "verified" earns the tier. */
|
|
119
|
+
export function trustTierFor(result) {
|
|
120
|
+
return result.status === "verified" ? "verified" : "unverified";
|
|
121
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FastifyInstance } from "fastify";
|
|
2
|
+
interface AdminDirResolution {
|
|
3
|
+
dir: string | null;
|
|
4
|
+
invalidOverride: string | undefined;
|
|
5
|
+
}
|
|
6
|
+
export declare function resolveAdminDir(): AdminDirResolution;
|
|
7
|
+
export declare function adminStaticRoutes(app: FastifyInstance): Promise<void>;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import fastifyStatic from "@fastify/static";
|
|
5
|
+
import { ADMIN_RATE_LIMIT, ipOnlyRateLimitConfig } from "../auth/route-rate-limits.js";
|
|
6
|
+
import { isAdminUiEnabled } from "../config/admin-ui.js";
|
|
7
|
+
// A usable admin directory: it must actually be a directory (not a file —
|
|
8
|
+
// @fastify/static@10.1.3 throws "root must be a directory" at registration
|
|
9
|
+
// otherwise, crashing buildApp()) and it must contain the SPA's index.html
|
|
10
|
+
// — an empty directory would otherwise resolve successfully and then serve
|
|
11
|
+
// bare per-route 404s instead of the clear 404-with-cause. index.html is
|
|
12
|
+
// also what the fallback route below reads on every deep link, so its
|
|
13
|
+
// presence is a hard precondition for this module, not a heuristic.
|
|
14
|
+
function isUsableAdminDir(candidate) {
|
|
15
|
+
try {
|
|
16
|
+
return statSync(candidate).isDirectory() && existsSync(join(candidate, "index.html"));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function resolveAdminDir() {
|
|
23
|
+
const override = process.env.VELORA_ADMIN_DIR;
|
|
24
|
+
if (override) {
|
|
25
|
+
// path.resolve() before anything else: @fastify/static@10.1.3 also
|
|
26
|
+
// throws ("root must be an absolute path") at registration when root
|
|
27
|
+
// is relative. Resolving against cwd up front means a relative
|
|
28
|
+
// override behaves the same as an absolute one instead of crashing
|
|
29
|
+
// boot.
|
|
30
|
+
const resolved = resolve(override);
|
|
31
|
+
if (isUsableAdminDir(resolved))
|
|
32
|
+
return { dir: resolved, invalidOverride: undefined };
|
|
33
|
+
return { dir: null, invalidOverride: override };
|
|
34
|
+
}
|
|
35
|
+
const here = dirname(fileURLToPath(import.meta.url)); // dist/routes or src/routes
|
|
36
|
+
const candidates = [
|
|
37
|
+
join(here, "..", "admin-bundle"),
|
|
38
|
+
join(here, "..", "..", "..", "..", "apps", "admin-ui", "dist-bundle"),
|
|
39
|
+
];
|
|
40
|
+
const dir = candidates.find((c) => isUsableAdminDir(c)) ?? null;
|
|
41
|
+
return { dir, invalidOverride: undefined };
|
|
42
|
+
}
|
|
43
|
+
// Local copies of docs-static.ts's private helpers rather than a shared
|
|
44
|
+
// export: that module deliberately keeps them private, and duplicating
|
|
45
|
+
// three tiny functions is cheaper than inventing a shared "degraded static
|
|
46
|
+
// response" abstraction for two callers.
|
|
47
|
+
function escapeHtml(text) {
|
|
48
|
+
return text
|
|
49
|
+
.replace(/&/g, "&")
|
|
50
|
+
.replace(/</g, "<")
|
|
51
|
+
.replace(/>/g, ">")
|
|
52
|
+
.replace(/"/g, """);
|
|
53
|
+
}
|
|
54
|
+
// /velora is opened directly in a browser tab (it IS the browser surface),
|
|
55
|
+
// so a degraded response there should be readable HTML, not raw JSON.
|
|
56
|
+
// Anything else (curl, tests, uptime probes) keeps the JSON shape.
|
|
57
|
+
function wantsHtml(req) {
|
|
58
|
+
return (req.headers.accept ?? "").includes("text/html");
|
|
59
|
+
}
|
|
60
|
+
function sendDegraded(req, reply, message) {
|
|
61
|
+
if (wantsHtml(req)) {
|
|
62
|
+
return reply
|
|
63
|
+
.code(404)
|
|
64
|
+
.type("text/html")
|
|
65
|
+
.send(`<!doctype html><html><head><meta charset="utf-8"><title>Admin unavailable</title></head>` +
|
|
66
|
+
`<body><h1>Admin unavailable</h1><p>${escapeHtml(message)}</p></body></html>`);
|
|
67
|
+
}
|
|
68
|
+
return reply.code(404).send({ message });
|
|
69
|
+
}
|
|
70
|
+
export async function adminStaticRoutes(app) {
|
|
71
|
+
// S162 precedent: static-ish reads, but not unbounded. Registered
|
|
72
|
+
// per-scope, matching auth.ts/docs-static.ts/plugin-api-routes.ts/
|
|
73
|
+
// public-api.ts — @fastify/rate-limit decorates the instance, so it is
|
|
74
|
+
// registered in child scopes OR at the root, never both.
|
|
75
|
+
//
|
|
76
|
+
// ipOnlyRateLimitConfig(), NOT the shared three-lane rateLimitConfig
|
|
77
|
+
// docs-static.ts uses. That is the one place this module cannot copy
|
|
78
|
+
// docs-static, and it is the S162-I6 constraint verbatim: auth/
|
|
79
|
+
// rate-limit.ts imports auth/api-keys.ts, which imports db/prisma.ts —
|
|
80
|
+
// a module with a top-level `await loadPrismaClient(...)`. This module is
|
|
81
|
+
// registered in buildSetupOnlyApp() too, the boot path documented as
|
|
82
|
+
// never touching Prisma and which runs precisely when nothing is
|
|
83
|
+
// configured yet, so importing the three-lane config here would drag
|
|
84
|
+
// Prisma into it and could fail that boot outright. setup.ts made the
|
|
85
|
+
// same swap for the same reason.
|
|
86
|
+
//
|
|
87
|
+
// The scope's ceiling is the ADMIN lane rather than the generic IP one,
|
|
88
|
+
// and this is where ADMIN_RATE_LIMIT actually earns its keep: with
|
|
89
|
+
// wildcard:false below, @fastify/static declares ~100 hashed asset
|
|
90
|
+
// routes in this scope and gives this plugin no way to attach per-route
|
|
91
|
+
// config to them (its internal routeOpts carries no `config` key), so
|
|
92
|
+
// the plugin-level default is the ONLY lever that reaches them. Leaving
|
|
93
|
+
// it at the generic 60/IP/minute would mean a cold load of the SPA plus
|
|
94
|
+
// one hard refresh could 429 the login screen.
|
|
95
|
+
await app.register(import("@fastify/rate-limit"), {
|
|
96
|
+
...ipOnlyRateLimitConfig(),
|
|
97
|
+
max: ADMIN_RATE_LIMIT.max(),
|
|
98
|
+
timeWindow: ADMIN_RATE_LIMIT.timeWindow(),
|
|
99
|
+
});
|
|
100
|
+
// This module's routes serve static HTML/asset infra, not Zod-validated
|
|
101
|
+
// JSON — api-design.md's "every route needs a request/response schema"
|
|
102
|
+
// rule governs the Zod→OpenAPI chain, which these deliberately sit
|
|
103
|
+
// outside of. Hidden from Swagger up front, same treatment docs-static.ts
|
|
104
|
+
// gives /docs. (@fastify/static hides its own generated routes already;
|
|
105
|
+
// this covers the ones declared below.) The hook is registered before any
|
|
106
|
+
// route in this scope so it sees all of them, and it is scoped to this
|
|
107
|
+
// encapsulated child, so `url === "/"` can only ever match the root
|
|
108
|
+
// redirect declared here.
|
|
109
|
+
app.addHook("onRoute", (routeOptions) => {
|
|
110
|
+
if (routeOptions.url.startsWith("/velora") || routeOptions.url === "/") {
|
|
111
|
+
routeOptions.schema = { ...routeOptions.schema, hide: true };
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
// Every route below already sits on the ADMIN lane by way of the
|
|
115
|
+
// plugin-level default registered above, so no per-route `config` is
|
|
116
|
+
// attached: repeating the same numbers on each route would suggest the
|
|
117
|
+
// asset routes (which cannot carry it) are on a different lane than they
|
|
118
|
+
// are.
|
|
119
|
+
// VELORA_ADMIN=off (the hosted demo's setting): the SPA is not served,
|
|
120
|
+
// but /velora/* still answers a 404 that NAMES the flag rather than the
|
|
121
|
+
// app-wide "route not found" — an operator who set the flag on one host
|
|
122
|
+
// and forgot it on another gets told why, which is the same
|
|
123
|
+
// 404-with-cause contract the missing-bundle branch honours. `/` is
|
|
124
|
+
// deliberately NOT registered in this branch, so the root keeps today's
|
|
125
|
+
// headless-bridge behaviour exactly.
|
|
126
|
+
if (!isAdminUiEnabled()) {
|
|
127
|
+
const message = "The admin UI is disabled on this instance (VELORA_ADMIN=off). The API is unaffected; " +
|
|
128
|
+
"unset VELORA_ADMIN (or set it to anything other than `off`) to serve the admin at /velora/.";
|
|
129
|
+
app.get("/velora", async (req, reply) => sendDegraded(req, reply, message));
|
|
130
|
+
app.get("/velora/", async (req, reply) => sendDegraded(req, reply, message));
|
|
131
|
+
app.get("/velora/*", async (req, reply) => sendDegraded(req, reply, message));
|
|
132
|
+
app.log.info("[admin-static] VELORA_ADMIN=off — the admin SPA is not served");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const { dir: adminDir, invalidOverride } = resolveAdminDir();
|
|
136
|
+
// Exact-path redirects. Fastify 5's Reply#redirect signature is
|
|
137
|
+
// (url, code), not (code, url) — verified against this repo's installed
|
|
138
|
+
// fastify@5.8.5 source (lib/reply.js), which differs from older majors.
|
|
139
|
+
// Both are registered before the missing-bundle branch returns: a
|
|
140
|
+
// degraded instance should still send you to the URL that explains
|
|
141
|
+
// itself, rather than silently keeping the pre-S184 root behaviour.
|
|
142
|
+
app.get("/velora", async (_req, reply) => reply.redirect("/velora/", 302));
|
|
143
|
+
app.get("/", async (_req, reply) => reply.redirect("/velora/", 302));
|
|
144
|
+
if (!adminDir) {
|
|
145
|
+
const message = invalidOverride
|
|
146
|
+
? `VELORA_ADMIN_DIR is set to "${invalidOverride}" but it is not usable (it must resolve to a ` +
|
|
147
|
+
"directory containing index.html). Fix the override or unset VELORA_ADMIN_DIR to fall back " +
|
|
148
|
+
"to the built-in bundle location."
|
|
149
|
+
: "The admin bundle is not installed. Expected it inside the server build " +
|
|
150
|
+
"(dist/admin-bundle) or at VELORA_ADMIN_DIR. Build with `turbo run build` to produce it.";
|
|
151
|
+
app.get("/velora/", async (req, reply) => sendDegraded(req, reply, message));
|
|
152
|
+
app.get("/velora/*", async (req, reply) => sendDegraded(req, reply, message));
|
|
153
|
+
app.log.warn("[admin-static] no admin bundle found — /velora/* will answer 404-with-cause");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// Registered directly on this scope (not in a further encapsulated
|
|
157
|
+
// child): unlike docs-static.ts, this module needs no setNotFoundHandler
|
|
158
|
+
// at all — every path under /velora/ is owned by a real route, so nothing
|
|
159
|
+
// here can leak a 404 shape onto /api/*.
|
|
160
|
+
//
|
|
161
|
+
// wildcard: false makes @fastify/static enumerate the bundle at
|
|
162
|
+
// registration and declare one route per file (plus HEAD), leaving deep
|
|
163
|
+
// links unmatched so the SPA fallback below owns them. The bundle is
|
|
164
|
+
// immutable and hashed, so a boot-time enumeration is exactly right.
|
|
165
|
+
//
|
|
166
|
+
// index: false is load-bearing, not cosmetic. With the default
|
|
167
|
+
// (index.html), the wildcard:false branch ALSO declares a route for each
|
|
168
|
+
// index directory — here "/velora/" — which collides head-on with the
|
|
169
|
+
// explicit "/velora/" route below (FST_ERR_DUPLICATED_ROUTE at boot).
|
|
170
|
+
// Turning it off keeps this module's own routes as the single owner of
|
|
171
|
+
// the SPA entry point, so the index is served through one code path
|
|
172
|
+
// whether it was requested at /velora/ or reached by fallback.
|
|
173
|
+
//
|
|
174
|
+
// decorateReply: false, as in docs-static.ts — nothing here needs
|
|
175
|
+
// reply.sendFile, and not decorating keeps this registration from ever
|
|
176
|
+
// colliding with another @fastify/static mount on the same instance.
|
|
177
|
+
await app.register(fastifyStatic, {
|
|
178
|
+
root: adminDir,
|
|
179
|
+
prefix: "/velora/",
|
|
180
|
+
wildcard: false,
|
|
181
|
+
index: false,
|
|
182
|
+
decorateReply: false,
|
|
183
|
+
});
|
|
184
|
+
// The SPA entry point, and the fallback that makes client-side routing
|
|
185
|
+
// survive a refresh or a pasted deep link. decorateReply: false means
|
|
186
|
+
// reply.sendFile isn't available here (docs-static.ts reads its 404.html
|
|
187
|
+
// the same way) — read index.html and send it per request (a single
|
|
188
|
+
// small file behind a rate limit). NOTE: unlike the docs bundle, a
|
|
189
|
+
// bundle swapped underneath a RUNNING server needs a restart — with
|
|
190
|
+
// wildcard: false the asset routes are enumerated once at registration,
|
|
191
|
+
// so a new build's hashed assets would have no routes.
|
|
192
|
+
const indexHtml = join(adminDir, "index.html");
|
|
193
|
+
const sendIndex = (reply) => reply.type("text/html").send(readFileSync(indexHtml));
|
|
194
|
+
app.get("/velora/", async (_req, reply) => sendIndex(reply));
|
|
195
|
+
app.get("/velora/*", async (_req, reply) => sendIndex(reply));
|
|
196
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ApiErrorSchema, ApiKeyListQuerySchema, ApiKeyListResponseSchema, CreateApiKeyRequestSchema, CreateApiKeyResponseSchema, RevokeApiKeyResponseSchema, } from "@velora-cms/api-schemas";
|
|
3
|
+
import { prisma } from "../db/prisma.js";
|
|
4
|
+
import { generateApiKey, hashApiKey } from "../auth/api-keys.js";
|
|
5
|
+
import { requireAdmin } from "../auth/require-auth.js";
|
|
6
|
+
const ApiKeyParamsSchema = z.object({ id: z.string().uuid() });
|
|
7
|
+
// Deliberately reconstructed field by field — keyHash must never ride
|
|
8
|
+
// along by spread (users.ts's toSummary precedent).
|
|
9
|
+
function toSummary(key) {
|
|
10
|
+
return {
|
|
11
|
+
id: key.id,
|
|
12
|
+
name: key.name,
|
|
13
|
+
scope: key.scope,
|
|
14
|
+
createdAt: key.createdAt.toISOString(),
|
|
15
|
+
lastUsedAt: key.lastUsedAt?.toISOString() ?? null,
|
|
16
|
+
revokedAt: key.revokedAt?.toISOString() ?? null,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
// Every route here is admin-only, like /api/users — key management is
|
|
20
|
+
// backoffice surface, JWT-gated; the public API these keys unlock is the
|
|
21
|
+
// separate /api/v1 namespace, and a key can never manage keys.
|
|
22
|
+
export const apiKeyRoutes = async (app) => {
|
|
23
|
+
app.get("/api/api-keys", {
|
|
24
|
+
onRequest: requireAdmin,
|
|
25
|
+
schema: {
|
|
26
|
+
querystring: ApiKeyListQuerySchema,
|
|
27
|
+
response: {
|
|
28
|
+
200: ApiKeyListResponseSchema,
|
|
29
|
+
401: ApiErrorSchema,
|
|
30
|
+
403: ApiErrorSchema,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
}, async (request) => {
|
|
34
|
+
const { cursor, limit } = request.query;
|
|
35
|
+
const rows = await prisma.apiKey.findMany({
|
|
36
|
+
orderBy: [{ createdAt: "asc" }, { id: "asc" }],
|
|
37
|
+
take: limit,
|
|
38
|
+
...(cursor ? { cursor: { id: cursor }, skip: 1 } : {}),
|
|
39
|
+
});
|
|
40
|
+
const nextCursor = rows.length === limit ? (rows[rows.length - 1]?.id ?? null) : null;
|
|
41
|
+
return { items: rows.map(toSummary), nextCursor };
|
|
42
|
+
});
|
|
43
|
+
app.post("/api/api-keys", {
|
|
44
|
+
onRequest: requireAdmin,
|
|
45
|
+
schema: {
|
|
46
|
+
body: CreateApiKeyRequestSchema,
|
|
47
|
+
response: {
|
|
48
|
+
201: CreateApiKeyResponseSchema,
|
|
49
|
+
401: ApiErrorSchema,
|
|
50
|
+
403: ApiErrorSchema,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
}, async (request, reply) => {
|
|
54
|
+
// The plaintext exists only in this scope and this response — only
|
|
55
|
+
// its hash is persisted, so it can never be shown again.
|
|
56
|
+
const secret = generateApiKey();
|
|
57
|
+
const row = await prisma.apiKey.create({
|
|
58
|
+
data: {
|
|
59
|
+
name: request.body.name,
|
|
60
|
+
// Zod defaults an omitted scope to "read" — a key can never
|
|
61
|
+
// become a write key by accident.
|
|
62
|
+
scope: request.body.scope,
|
|
63
|
+
keyHash: hashApiKey(secret),
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
return reply.code(201).send({ apiKey: toSummary(row), secret });
|
|
67
|
+
});
|
|
68
|
+
app.post("/api/api-keys/:id/revoke", {
|
|
69
|
+
onRequest: requireAdmin,
|
|
70
|
+
schema: {
|
|
71
|
+
params: ApiKeyParamsSchema,
|
|
72
|
+
response: {
|
|
73
|
+
200: RevokeApiKeyResponseSchema,
|
|
74
|
+
401: ApiErrorSchema,
|
|
75
|
+
403: ApiErrorSchema,
|
|
76
|
+
404: ApiErrorSchema,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
}, async (request, reply) => {
|
|
80
|
+
const existing = await prisma.apiKey.findUnique({ where: { id: request.params.id } });
|
|
81
|
+
if (!existing) {
|
|
82
|
+
return reply.code(404).send({ message: `No API key found for id ${request.params.id}` });
|
|
83
|
+
}
|
|
84
|
+
// Idempotent: revoking an already-revoked key returns it unchanged —
|
|
85
|
+
// the caller's goal (key unusable) is already true, and keeping the
|
|
86
|
+
// original revokedAt preserves the audit trail.
|
|
87
|
+
const row = existing.revokedAt
|
|
88
|
+
? existing
|
|
89
|
+
: await prisma.apiKey.update({
|
|
90
|
+
where: { id: existing.id },
|
|
91
|
+
data: { revokedAt: new Date() },
|
|
92
|
+
});
|
|
93
|
+
return { apiKey: toSummary(row) };
|
|
94
|
+
});
|
|
95
|
+
};
|