@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,283 @@
|
|
|
1
|
+
import { hashApiKey, isApiKeyHashValid } from "./api-keys.js";
|
|
2
|
+
import { parseWindow } from "./route-rate-limits.js";
|
|
3
|
+
// Same structural rule as the jwt: lane below applies here too: a rate-
|
|
4
|
+
// limit bucket key must derive from something the caller cannot freely
|
|
5
|
+
// mint. `vk_<random-hex>` is trivially self-mintable (it's just the
|
|
6
|
+
// KEY_PREFIX plus 32 random bytes — see generateApiKey), so bucketing on
|
|
7
|
+
// an unresolved token's hash would let an anonymous caller on a `public`
|
|
8
|
+
// route rotate the random suffix every request, minting a fresh key:
|
|
9
|
+
// bucket (its own 100/window allowance) each time — unbounded in
|
|
10
|
+
// aggregate, and never rejected, since a public route never calls
|
|
11
|
+
// requireApiKey. So a presented vk_ token must actually resolve to a
|
|
12
|
+
// live (unrevoked) key before it earns the key: lane; anything else
|
|
13
|
+
// falls back to the capped ip: lane, exactly like the jwt: lane's
|
|
14
|
+
// unverifiable-token case.
|
|
15
|
+
//
|
|
16
|
+
// Resolving means a DB lookup (isApiKeyHashValid, the exact same query
|
|
17
|
+
// requireApiKey itself uses — see api-keys.ts's findLiveApiKeyByHash).
|
|
18
|
+
// So results are cached in-process, keyed by hash, for a short TTL: a
|
|
19
|
+
// real key's positive result is cached too, which is what lets the
|
|
20
|
+
// honest hot path — repeat calls presenting the SAME real key — mostly
|
|
21
|
+
// avoid the DB. The TTL is also why a freshly-revoked key can keep
|
|
22
|
+
// earning its own bucket for up to apiKeyCacheTtlMs() after revocation —
|
|
23
|
+
// that does NOT grant access; requireApiKey re-checks the DB (no caching
|
|
24
|
+
// there) on every request and rejects it immediately regardless of what
|
|
25
|
+
// bucket the rate limiter put it in.
|
|
26
|
+
//
|
|
27
|
+
// IMPORTANT: this cache does NOT and cannot make a rotating-token flood
|
|
28
|
+
// free. Every distinct fake vk_ token is a distinct hash, so it's a
|
|
29
|
+
// guaranteed cache MISS — one DB query and, previously, one new map
|
|
30
|
+
// entry, every single time, for the flood's whole duration; a positive
|
|
31
|
+
// cache only pays off when a hash repeats. So a hard size ceiling
|
|
32
|
+
// (MAX_API_KEY_CACHE_ENTRIES / apiKeyCacheMaxEntries() below) bounds the
|
|
33
|
+
// map's memory regardless of hit rate: once at the cap, a cache MISS's
|
|
34
|
+
// result is still computed and returned (the request still gets an
|
|
35
|
+
// answer — a busy but correctly-behaving cache is never a reason to
|
|
36
|
+
// misclassify a token), it's just not inserted, so the map can't be
|
|
37
|
+
// forced to grow past the cap by an attacker who never repeats a token.
|
|
38
|
+
// That does mean a sustained flood past the cap still costs one DB query
|
|
39
|
+
// per request forever — the cap protects memory, not query volume; query
|
|
40
|
+
// volume protection would need a different mechanism (e.g. IP-scoped
|
|
41
|
+
// throttling ahead of this lookup), which is out of scope here.
|
|
42
|
+
//
|
|
43
|
+
// Both knobs are env-configurable and evaluated per-cache-write
|
|
44
|
+
// (parsePositiveInt below, same "defensive parse, not frozen at
|
|
45
|
+
// module-import time" shape as configuredMax/configuredIpMax further
|
|
46
|
+
// down this file) — mainly so a test can force small values and assert
|
|
47
|
+
// deterministically, rather than racing real time or filling thousands
|
|
48
|
+
// of real entries.
|
|
49
|
+
function apiKeyCacheTtlMs() {
|
|
50
|
+
return parsePositiveInt(process.env.VELORA_RATE_LIMIT_KEY_CACHE_TTL_MS, 30_000);
|
|
51
|
+
}
|
|
52
|
+
function apiKeyCacheMaxEntries() {
|
|
53
|
+
return parsePositiveInt(process.env.VELORA_RATE_LIMIT_KEY_CACHE_MAX_ENTRIES, 5_000);
|
|
54
|
+
}
|
|
55
|
+
const apiKeyValidityCache = new Map();
|
|
56
|
+
// Opportunistic eviction (called on every cache write, not on a timer):
|
|
57
|
+
// a bounded cache doesn't need a background sweep if every insert first
|
|
58
|
+
// clears out anything that's already expired, so the map never holds
|
|
59
|
+
// more live entries than "distinct hashes seen within the last TTL
|
|
60
|
+
// window" — which keeps a MODERATE, mostly-repeating flow of traffic
|
|
61
|
+
// from growing this map forever. It does NOT bound a flood of
|
|
62
|
+
// never-repeating fake tokens within a single TTL window (nothing in
|
|
63
|
+
// that flood expires until the TTL elapses) — that's what the hard size
|
|
64
|
+
// cap above is for; the two mechanisms cover different failure shapes.
|
|
65
|
+
function pruneExpiredApiKeyCacheEntries(now) {
|
|
66
|
+
for (const [hash, entry] of apiKeyValidityCache) {
|
|
67
|
+
if (entry.expiresAt <= now) {
|
|
68
|
+
apiKeyValidityCache.delete(hash);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Test-only accessor (no production caller) so a test can pin "the cache
|
|
73
|
+
// never grows past its configured cap" without reaching into
|
|
74
|
+
// module-private state via a hack.
|
|
75
|
+
export function apiKeyValidityCacheSizeForTests() {
|
|
76
|
+
return apiKeyValidityCache.size;
|
|
77
|
+
}
|
|
78
|
+
async function isVkTokenValid(token) {
|
|
79
|
+
const hash = hashApiKey(token);
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
const cached = apiKeyValidityCache.get(hash);
|
|
82
|
+
if (cached && cached.expiresAt > now) {
|
|
83
|
+
return { hash, valid: cached.valid };
|
|
84
|
+
}
|
|
85
|
+
let valid;
|
|
86
|
+
try {
|
|
87
|
+
valid = await isApiKeyHashValid(hash);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// Fail safe, symmetric with the jwt: branch's catch below: a DB
|
|
91
|
+
// error here (down, pool exhausted — plausibly BY a flood the cache
|
|
92
|
+
// above is trying to blunt) must not propagate out of keyGenerator —
|
|
93
|
+
// this runs as an onRequest hook ahead of EVERY request through
|
|
94
|
+
// either rate-limited scope, including public plugin routes that may
|
|
95
|
+
// not touch the DB at all, so an uncaught throw here would 500 all
|
|
96
|
+
// of them. An unresolved-on-error token is treated exactly like an
|
|
97
|
+
// unresolved-on-miss token: capped ip: lane, never key:. Deliberately
|
|
98
|
+
// NOT cached — a transient outage isn't "this key is invalid," and
|
|
99
|
+
// caching a false negative would keep rejecting a real key for the
|
|
100
|
+
// whole TTL after the DB recovers.
|
|
101
|
+
return { hash, valid: false };
|
|
102
|
+
}
|
|
103
|
+
pruneExpiredApiKeyCacheEntries(now);
|
|
104
|
+
if (apiKeyValidityCache.size < apiKeyCacheMaxEntries()) {
|
|
105
|
+
apiKeyValidityCache.set(hash, { valid, expiresAt: now + apiKeyCacheTtlMs() });
|
|
106
|
+
}
|
|
107
|
+
return { hash, valid };
|
|
108
|
+
}
|
|
109
|
+
// Per-API-key rate limiting for the external surface (Session 108),
|
|
110
|
+
// extended to three lanes by Session 160's public access tier. The bucket
|
|
111
|
+
// key is derived from what the request actually presents:
|
|
112
|
+
// - a `vk_` bearer token that RESOLVES to a real, non-revoked key (see
|
|
113
|
+
// isVkTokenValid above) -> `key:<hash>` — a real external API-key
|
|
114
|
+
// caller. An unresolved vk_-prefixed token (unknown or revoked) is
|
|
115
|
+
// never trusted with this lane — see the comment above the cache.
|
|
116
|
+
// - any OTHER bearer token that SIGNATURE-VERIFIES as a real session JWT
|
|
117
|
+
// -> `jwt:<sub>` — a logged-in admin/editor session hitting a
|
|
118
|
+
// pluginApiRoutes catch-all, bucketed per user id (not per token
|
|
119
|
+
// string, so a token refresh doesn't mint a fresh bucket). jwtVerify()
|
|
120
|
+
// only proves a valid SESSION, not an admin role — role gating is
|
|
121
|
+
// requireAdmin's job, later; this lane exists for "not anonymous,"
|
|
122
|
+
// not "is an admin." This has to be its own lane, not folded into
|
|
123
|
+
// `ip:`, because S108's design is "admin traffic is not rate-limited"
|
|
124
|
+
// — if it shared the anonymous IP lane, every admin behind one office
|
|
125
|
+
// IP/NAT would share one small budget.
|
|
126
|
+
// - anything else (no bearer token, an unresolved vk_ token, or a
|
|
127
|
+
// bearer token that is neither `vk_`-prefixed nor a verifiable JWT —
|
|
128
|
+
// garbage, expired, forged) -> `ip:${request.ip}`. This is the
|
|
129
|
+
// security-critical branch: this function runs as `keyGenerator`, an
|
|
130
|
+
// onRequest hook that fires BEFORE requireAdmin/requireApiKey ever run
|
|
131
|
+
// (dispatch order in plugin-api-routes.ts resolves the route and its
|
|
132
|
+
// access level only after the rate limiter has already picked a
|
|
133
|
+
// bucket). If an unresolved/unverified bearer token were trusted
|
|
134
|
+
// enough to land in key: or jwt:, an anonymous caller hitting a
|
|
135
|
+
// `public` route with an arbitrary `Authorization` header would buy
|
|
136
|
+
// one of the higher-ceiling lanes and defeat the whole point of
|
|
137
|
+
// capping ip: — so a token must actually resolve/verify before
|
|
138
|
+
// anything is trusted as non-anonymous; any failure (or no token at
|
|
139
|
+
// all) falls back to the capped, anonymous ip: lane.
|
|
140
|
+
//
|
|
141
|
+
// Async because both isVkTokenValid (a DB lookup on cache miss) and
|
|
142
|
+
// request.jwtVerify() are async; @fastify/rate-limit awaits keyGenerator
|
|
143
|
+
// (its own index.js does `await params.keyGenerator(req)`), so this is a
|
|
144
|
+
// supported shape, not a workaround.
|
|
145
|
+
export async function rateLimitKey(request) {
|
|
146
|
+
const header = request.headers.authorization;
|
|
147
|
+
if (header?.startsWith("Bearer ")) {
|
|
148
|
+
const token = header.slice("Bearer ".length).trim();
|
|
149
|
+
if (token.startsWith("vk_")) {
|
|
150
|
+
const { hash, valid } = await isVkTokenValid(token);
|
|
151
|
+
if (valid) {
|
|
152
|
+
return `key:${hash}`;
|
|
153
|
+
}
|
|
154
|
+
return `ip:${request.ip}`;
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
await request.jwtVerify();
|
|
158
|
+
// S160 final-fix, finding 7: jwtVerify() only proves the SIGNATURE is
|
|
159
|
+
// valid, not that the payload carries a usable `sub` — a verified
|
|
160
|
+
// token with no sub (or an empty one) would otherwise produce the
|
|
161
|
+
// literal key `jwt:undefined`, which rateLimitConfig.max grants
|
|
162
|
+
// Number.MAX_SAFE_INTEGER: the one remaining place in this lane
|
|
163
|
+
// logic where the unlimited bucket was reachable without the thing
|
|
164
|
+
// that's supposed to earn it. Access tokens always carry `sub`
|
|
165
|
+
// today, so this is a guard against a future/malformed token, not a
|
|
166
|
+
// live gap — but it's the same class of bug this file's rateLimitKey
|
|
167
|
+
// comment above already documents fixing twice for the vk_ lane.
|
|
168
|
+
const sub = request.user?.sub;
|
|
169
|
+
if (typeof sub === "string" && sub.length > 0) {
|
|
170
|
+
return `jwt:${sub}`;
|
|
171
|
+
}
|
|
172
|
+
// No usable sub — fall through to the capped ip: lane below, same as
|
|
173
|
+
// an unverifiable token.
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// Unverifiable bearer token (garbage, expired, forged, wrong
|
|
177
|
+
// secret) — never trusted as a session; falls through to the
|
|
178
|
+
// capped anonymous lane below, same as presenting no token at all.
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return `ip:${request.ip}`;
|
|
182
|
+
}
|
|
183
|
+
// max/timeWindow are env-configurable so an operator can tune them without
|
|
184
|
+
// a code change; the defaults are a sane external-API baseline. `max` is a
|
|
185
|
+
// function (the plugin's supported `(req, key) => number` form) rather than
|
|
186
|
+
// a top-level `Number(process.env...)` computed once at module-import time,
|
|
187
|
+
// so the value reflects the env at the moment each request is checked —
|
|
188
|
+
// this module is statically imported (via app.ts) well before a test's
|
|
189
|
+
// `beforeAll` gets a chance to override the env var, so a plain constant
|
|
190
|
+
// would freeze in the default forever. `timeWindow` stays a plain string:
|
|
191
|
+
// the plugin's function form for it must return a number of milliseconds,
|
|
192
|
+
// not a duration string, and no test needs to override the window. Do NOT
|
|
193
|
+
// restructure it into a function to "match" `max` — that's a deliberate,
|
|
194
|
+
// documented asymmetry, not an oversight.
|
|
195
|
+
//
|
|
196
|
+
// Parsed defensively: an empty-string env var Numbers to 0 and a typo
|
|
197
|
+
// Numbers to NaN, and this lane (like the ip: lane below) now gates real
|
|
198
|
+
// traffic where a silently-adopted 0 is an outage, not a config no-op — so
|
|
199
|
+
// anything that doesn't parse to a positive finite number falls back to
|
|
200
|
+
// the default instead of being trusted verbatim.
|
|
201
|
+
function parsePositiveInt(value, fallback) {
|
|
202
|
+
const parsed = Number(value);
|
|
203
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
204
|
+
}
|
|
205
|
+
// S162 second-review fix: `timeWindow` below was still a bare `?? "1
|
|
206
|
+
// minute"` — the same defect `parseWindow` (route-rate-limits.ts) was
|
|
207
|
+
// written to prevent, left out of the first fix wave despite that wave's
|
|
208
|
+
// own commit title ("Defend rate-limit *_WINDOW env vars the same way
|
|
209
|
+
// *_MAX already is"). This is the LARGEST blast radius of any `timeWindow`
|
|
210
|
+
// in the system: it's the default for every scope that doesn't override it
|
|
211
|
+
// — auth (besides login), docs, plugin-api-routes, and public-api — so a
|
|
212
|
+
// blank or unparseable VELORA_RATE_LIMIT_WINDOW here would 500 most of the
|
|
213
|
+
// API, not just one family.
|
|
214
|
+
//
|
|
215
|
+
// Exported and factored out of the object literal below (rather than
|
|
216
|
+
// inlined as `parseWindow(process.env.VELORA_RATE_LIMIT_WINDOW, "1
|
|
217
|
+
// minute")` directly) so a test can exercise the exact parse logic
|
|
218
|
+
// `rateLimitConfig.timeWindow` is built from without re-importing this
|
|
219
|
+
// whole module — this module is statically imported by app.ts and pulls in
|
|
220
|
+
// api-keys.ts -> db/prisma.ts, which opens a real Prisma client at
|
|
221
|
+
// top-level `await`; re-importing it via vi.resetModules() to test a
|
|
222
|
+
// different env value would mean standing up a second live client
|
|
223
|
+
// mid-suite, which is worse than the thing being tested.
|
|
224
|
+
export function resolveGlobalTimeWindow(value) {
|
|
225
|
+
return parseWindow(value, "1 minute");
|
|
226
|
+
}
|
|
227
|
+
function configuredMax() {
|
|
228
|
+
return parsePositiveInt(process.env.VELORA_RATE_LIMIT_MAX, 100);
|
|
229
|
+
}
|
|
230
|
+
// Same env-configurable, evaluated-per-request shape as configuredMax()
|
|
231
|
+
// above, and for the same reason — this module is statically imported
|
|
232
|
+
// well before a test's beforeAll gets a chance to override the env var.
|
|
233
|
+
function configuredIpMax() {
|
|
234
|
+
return parsePositiveInt(process.env.VELORA_RATE_LIMIT_IP_MAX, 60);
|
|
235
|
+
}
|
|
236
|
+
export const rateLimitConfig = {
|
|
237
|
+
// Three lanes, three ceilings — rateLimitKey's doc comment explains why
|
|
238
|
+
// each bucket exists; this is why each gets the ceiling it gets:
|
|
239
|
+
// - `key:` (a vk_ token that actually RESOLVED to a real, non-revoked
|
|
240
|
+
// key — see isVkTokenValid) — configuredMax(), the original Session
|
|
241
|
+
// 108 external-API budget. An unresolved vk_-shaped token never
|
|
242
|
+
// reaches this lane, so this ceiling can't be bought by minting
|
|
243
|
+
// arbitrary vk_-prefixed strings.
|
|
244
|
+
// - `jwt:` (a signature-verified session, bucketed per user id) — left
|
|
245
|
+
// effectively unlimited, preserving S108's recorded intent
|
|
246
|
+
// ("admin-session traffic is not rate-limited"). It got its own lane
|
|
247
|
+
// specifically so it would NOT inherit the ip: ceiling below — and
|
|
248
|
+
// rateLimitKey only ever mints a jwt: bucket after request.jwtVerify()
|
|
249
|
+
// actually succeeds, never on the mere presence of a bearer token, so
|
|
250
|
+
// this ceiling can't be bought with a forged/garbage header. (This
|
|
251
|
+
// lane is about "logged in," not "admin" — role gating still happens
|
|
252
|
+
// downstream in requireAdmin.)
|
|
253
|
+
// - `ip:` (no credentials at all, an unresolved/unknown/revoked vk_
|
|
254
|
+
// token, or a bearer token that failed verification) —
|
|
255
|
+
// configuredIpMax(). Used to be effectively unlimited too, back when
|
|
256
|
+
// un-keyed traffic could only ever BE an admin session hitting a
|
|
257
|
+
// pluginApiRoutes catch-all with no vk_ bearer token. Session 160's
|
|
258
|
+
// "public" access tier broke that premise: genuinely anonymous,
|
|
259
|
+
// un-credentialed browser traffic (e.g. a contact-form submission)
|
|
260
|
+
// now lands here too, so this lane gets a real, deliberately
|
|
261
|
+
// tighter ceiling instead.
|
|
262
|
+
max: (_req, key) => {
|
|
263
|
+
if (key.startsWith("key:"))
|
|
264
|
+
return configuredMax();
|
|
265
|
+
if (key.startsWith("jwt:"))
|
|
266
|
+
return Number.MAX_SAFE_INTEGER;
|
|
267
|
+
return configuredIpMax();
|
|
268
|
+
},
|
|
269
|
+
timeWindow: resolveGlobalTimeWindow(process.env.VELORA_RATE_LIMIT_WINDOW),
|
|
270
|
+
keyGenerator: rateLimitKey,
|
|
271
|
+
// Return the codebase's uniform { message } error body (ApiErrorSchema
|
|
272
|
+
// shape) instead of the plugin's default; Retry-After is set by the
|
|
273
|
+
// plugin automatically. The plugin `throw`s whatever this returns
|
|
274
|
+
// (index.js: `throw params.errorResponseBuilder(req, respCtx)`) and
|
|
275
|
+
// Fastify's default error handler replies using `error.statusCode`, so
|
|
276
|
+
// `statusCode` must be carried through on the returned object — without
|
|
277
|
+
// it every rate-limited request would 500 instead of 429 (or 403 for a
|
|
278
|
+
// ban, hence reusing context.statusCode rather than hardcoding 429).
|
|
279
|
+
errorResponseBuilder: (_request, context) => ({
|
|
280
|
+
statusCode: context.statusCode,
|
|
281
|
+
message: `Rate limit exceeded, retry in ${context.after}`,
|
|
282
|
+
}),
|
|
283
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { FastifyReply, FastifyRequest } from "fastify";
|
|
2
|
+
import "./jwt-payload.js";
|
|
3
|
+
export declare function requireAuth(request: FastifyRequest, reply: FastifyReply): Promise<void>;
|
|
4
|
+
export declare function requireAdmin(request: FastifyRequest, reply: FastifyReply): Promise<void>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import "./jwt-payload.js";
|
|
2
|
+
// onRequest hook, not preHandler: Fastify validates the body before
|
|
3
|
+
// preHandler runs, so a preHandler auth check would answer unauthenticated
|
|
4
|
+
// requests with validation 400s instead of a 401.
|
|
5
|
+
export async function requireAuth(request, reply) {
|
|
6
|
+
try {
|
|
7
|
+
await request.jwtVerify();
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
await reply.code(401).send({ message: "Authentication required" });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
// Role gate on top of requireAuth. The role rides in the JWT — good for
|
|
14
|
+
// 15 minutes (the access token TTL), so a demotion takes effect on the
|
|
15
|
+
// next token refresh, not instantly. Acceptable for v0.1; revisit if
|
|
16
|
+
// instant revocation ever matters.
|
|
17
|
+
export async function requireAdmin(request, reply) {
|
|
18
|
+
try {
|
|
19
|
+
await request.jwtVerify();
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
await reply.code(401).send({ message: "Authentication required" });
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (request.user.role !== "admin") {
|
|
26
|
+
await reply.code(403).send({ message: "Admin role required" });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { FastifyRequest } from "fastify";
|
|
2
|
+
export declare function parseWindow(value: string | undefined, fallback: string): string;
|
|
3
|
+
export declare const LOGIN_RATE_LIMIT: {
|
|
4
|
+
max: () => number;
|
|
5
|
+
timeWindow: () => string;
|
|
6
|
+
};
|
|
7
|
+
export declare const SETUP_RATE_LIMIT: {
|
|
8
|
+
max: () => number;
|
|
9
|
+
timeWindow: () => string;
|
|
10
|
+
};
|
|
11
|
+
export declare const DOCS_RATE_LIMIT: {
|
|
12
|
+
max: () => number;
|
|
13
|
+
timeWindow: () => string;
|
|
14
|
+
};
|
|
15
|
+
export declare const ADMIN_RATE_LIMIT: {
|
|
16
|
+
max: () => number;
|
|
17
|
+
timeWindow: () => string;
|
|
18
|
+
};
|
|
19
|
+
export declare function ipOnlyKeyGenerator(request: FastifyRequest): Promise<string>;
|
|
20
|
+
export declare function ipOnlyRateLimitConfig(): {
|
|
21
|
+
max: number;
|
|
22
|
+
timeWindow: string;
|
|
23
|
+
keyGenerator: typeof ipOnlyKeyGenerator;
|
|
24
|
+
errorResponseBuilder: (request: FastifyRequest, context: {
|
|
25
|
+
after: string;
|
|
26
|
+
statusCode: number;
|
|
27
|
+
}) => {
|
|
28
|
+
statusCode: number;
|
|
29
|
+
message: string;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { parse as parseMs } from "@lukeed/ms";
|
|
2
|
+
// S162: per-family ceilings for the public route families that had none.
|
|
3
|
+
// The shared three-lane rateLimitConfig (auth/rate-limit.ts) stays the
|
|
4
|
+
// default for everything else; these are overrides for routes whose
|
|
5
|
+
// economics differ from a generic API call.
|
|
6
|
+
//
|
|
7
|
+
// Same "parse at call time, not at import time" shape as rate-limit.ts's
|
|
8
|
+
// configuredMax/configuredIpMax, and for the same reason: this module is
|
|
9
|
+
// statically imported well before a test's beforeAll can set an env var,
|
|
10
|
+
// so a plain constant would freeze the default in forever.
|
|
11
|
+
function parsePositiveInt(value, fallback) {
|
|
12
|
+
const parsed = Number(value);
|
|
13
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
14
|
+
}
|
|
15
|
+
// S162 final review, I3: `max` was defended by parsePositiveInt above, but
|
|
16
|
+
// every `timeWindow` read was a bare `?? "fallback"`, and `??` does not
|
|
17
|
+
// catch the empty string. @fastify/rate-limit hands whatever string this
|
|
18
|
+
// returns straight to @lukeed/ms's own `parse()` (mergeParams takes the
|
|
19
|
+
// `typeof === 'string'` branch without re-checking it parses), and an
|
|
20
|
+
// unparseable value (blank, a typo, anything parse() can't match) becomes
|
|
21
|
+
// `undefined` — which applyRateLimit then calls as a function, OUTSIDE its
|
|
22
|
+
// try/catch, 500ing the whole scope. A blank
|
|
23
|
+
// VELORA_RATE_LIMIT_LOGIN_WINDOW would mean nobody can log in, silently.
|
|
24
|
+
// Running the candidate through the SAME parser @fastify/rate-limit will
|
|
25
|
+
// use (rather than guessing at what looks like a duration) is what makes
|
|
26
|
+
// this defensible: anything parse() can't turn into a positive number of
|
|
27
|
+
// milliseconds falls back, exactly like a bad `max` value does above.
|
|
28
|
+
// Exported so rate-limit.ts's shared, three-lane rateLimitConfig can defend
|
|
29
|
+
// its own `timeWindow` (the biggest blast radius of all — it governs the
|
|
30
|
+
// auth, docs, plugin-api-routes and public-api scopes) with the exact same
|
|
31
|
+
// parser, rather than duplicating this logic or leaving it undefended.
|
|
32
|
+
export function parseWindow(value, fallback) {
|
|
33
|
+
if (value === undefined)
|
|
34
|
+
return fallback;
|
|
35
|
+
const parsed = parseMs(value);
|
|
36
|
+
return typeof parsed === "number" && parsed > 0 ? value : fallback;
|
|
37
|
+
}
|
|
38
|
+
// Deliberately the tightest ceiling in the system. POST /api/auth/login is
|
|
39
|
+
// unauthenticated and password-guessing is exactly what it invites.
|
|
40
|
+
export const LOGIN_RATE_LIMIT = {
|
|
41
|
+
max: () => parsePositiveInt(process.env.VELORA_RATE_LIMIT_LOGIN, 10),
|
|
42
|
+
timeWindow: () => parseWindow(process.env.VELORA_RATE_LIMIT_LOGIN_WINDOW, "15 minutes"),
|
|
43
|
+
};
|
|
44
|
+
// Runs once per install, and 409s outright on an already-configured
|
|
45
|
+
// instance (S151). The ceiling makes the pre-configuration window
|
|
46
|
+
// uninteresting to probe.
|
|
47
|
+
export const SETUP_RATE_LIMIT = {
|
|
48
|
+
max: () => parsePositiveInt(process.env.VELORA_RATE_LIMIT_SETUP, 5),
|
|
49
|
+
timeWindow: () => parseWindow(process.env.VELORA_RATE_LIMIT_SETUP_WINDOW, "1 hour"),
|
|
50
|
+
};
|
|
51
|
+
// Static-ish reads. Generous, but not unbounded.
|
|
52
|
+
export const DOCS_RATE_LIMIT = {
|
|
53
|
+
max: () => parsePositiveInt(process.env.VELORA_RATE_LIMIT_DOCS, 120),
|
|
54
|
+
timeWindow: () => parseWindow(process.env.VELORA_RATE_LIMIT_DOCS_WINDOW, "1 minute"),
|
|
55
|
+
};
|
|
56
|
+
// The admin SPA's static bundle at /velora (S184) — same reasoning and
|
|
57
|
+
// defaults as the docs bundle above. admin-static.ts applies this lane
|
|
58
|
+
// PLUGIN-LEVEL (the limiter registered on its encapsulated scope with
|
|
59
|
+
// these values), because @fastify/static's generated asset routes carry
|
|
60
|
+
// no per-route config slot — so EVERYTHING under /velora, hashed assets
|
|
61
|
+
// included, buckets on this lane, per IP. Do not "restore" per-route
|
|
62
|
+
// config there: it cannot reach the asset routes and would silently drop
|
|
63
|
+
// them to the generic IP lane (a cold SPA load could 429 the login).
|
|
64
|
+
export const ADMIN_RATE_LIMIT = {
|
|
65
|
+
max: () => parsePositiveInt(process.env.VELORA_RATE_LIMIT_ADMIN, 120),
|
|
66
|
+
timeWindow: () => parseWindow(process.env.VELORA_RATE_LIMIT_ADMIN_WINDOW, "1 minute"),
|
|
67
|
+
};
|
|
68
|
+
// The shared rateLimitKey puts a VERIFIED JWT into the jwt: lane, which is
|
|
69
|
+
// Number.MAX_SAFE_INTEGER — effectively unlimited. Login must never inherit
|
|
70
|
+
// that, or anyone holding one valid session could brute-force passwords
|
|
71
|
+
// without limit simply by attaching their own token. So login buckets on IP
|
|
72
|
+
// and nothing else.
|
|
73
|
+
//
|
|
74
|
+
// And on IP ONLY — never the submitted email. A per-email bucket would let
|
|
75
|
+
// an attacker distinguish real accounts from fake ones by watching which
|
|
76
|
+
// addresses start returning 429, turning the limiter itself into an
|
|
77
|
+
// account-existence oracle. Same discipline as S160's honeypot: the
|
|
78
|
+
// observable response must not vary with whether the subject exists.
|
|
79
|
+
//
|
|
80
|
+
// Shared beyond login as of S162's I6 fix: the setup scope (setup.ts)
|
|
81
|
+
// reuses this same generator for the same reason — no API keys or JWT
|
|
82
|
+
// sessions exist to lane there either, so IP-alone is the only sound key.
|
|
83
|
+
// The key prefix dropped "login-" accordingly now that it's not login-
|
|
84
|
+
// specific.
|
|
85
|
+
//
|
|
86
|
+
// That change means this generator now mints the literal string `ip:<addr>`
|
|
87
|
+
// for both login and setup — the same literal rate-limit.ts's own
|
|
88
|
+
// three-lane rateLimitKey mints for its unauthenticated fallback lane. That
|
|
89
|
+
// collision is safe, not accidental-looking-safe: `@fastify/rate-limit`
|
|
90
|
+
// registers a fresh `LocalStore` per scope it's registered on
|
|
91
|
+
// (`LocalStore.child()`), so a bucket key is only ever compared against
|
|
92
|
+
// other keys within the SAME scope's store. Login (auth.ts) and setup
|
|
93
|
+
// (setup.ts) are different registered scopes from each other and from
|
|
94
|
+
// every other route family, so two identical `ip:1.2.3.4` strings in
|
|
95
|
+
// different scopes address entirely separate maps — same key text, disjoint
|
|
96
|
+
// storage, no shared budget.
|
|
97
|
+
export async function ipOnlyKeyGenerator(request) {
|
|
98
|
+
return `ip:${request.ip}`;
|
|
99
|
+
}
|
|
100
|
+
// S162 final review, I6: setup.ts used to register the full three-lane
|
|
101
|
+
// rateLimitConfig (auth/rate-limit.ts) for its scope, which imports
|
|
102
|
+
// auth/api-keys.ts, which imports db/prisma.ts — a module with a
|
|
103
|
+
// top-level `await loadPrismaClient(...)`. That drags Prisma into the
|
|
104
|
+
// setup-only boot path (setup-app.ts's buildSetupOnlyApp), which is
|
|
105
|
+
// documented as never touching it, and which runs precisely when no
|
|
106
|
+
// database is configured yet — so an unconfigured VELORA_PRISMA_CLIENT_DIR
|
|
107
|
+
// pointing at a not-yet-generated client would throw at import time and
|
|
108
|
+
// prevent that boot path from starting at all.
|
|
109
|
+
//
|
|
110
|
+
// Setup has no API keys and no JWT sessions to lane in the first place —
|
|
111
|
+
// the three-lane generator buys it nothing. This is the plain IP-keyed
|
|
112
|
+
// equivalent, kept in this file (not auth/rate-limit.ts) so importing it
|
|
113
|
+
// can never pull Prisma in transitively.
|
|
114
|
+
// S165 audit fix: unlike LOGIN/SETUP/DOCS_RATE_LIMIT above, this config isn't
|
|
115
|
+
// dereferenced by a caller that calls `.timeWindow()` before handing a value
|
|
116
|
+
// to the plugin — setup.ts registers the WHOLE object directly as
|
|
117
|
+
// `@fastify/rate-limit`'s plugin options. That plugin's `timeWindow` option
|
|
118
|
+
// only accepts a plain string/number, or a function called as
|
|
119
|
+
// `timeWindow(req, key)` that itself must RETURN a number of milliseconds
|
|
120
|
+
// (see rateLimitConfig's own comment in rate-limit.ts for the full type). A
|
|
121
|
+
// zero-argument function returning a duration STRING — what this used to be
|
|
122
|
+
// — is neither: tsc rejects the assignment, and at runtime the plugin would
|
|
123
|
+
// take the function branch, call it as `timeWindow(req, key)`, and get
|
|
124
|
+
// `"1 minute"` back where it expects a number.
|
|
125
|
+
//
|
|
126
|
+
// Turned into a builder, called at the exact point of registration
|
|
127
|
+
// (setup.ts's `ipOnlyRateLimitConfig()`), so the env is still read at
|
|
128
|
+
// registration time rather than frozen at this module's import — this
|
|
129
|
+
// module is statically imported well before a test's beforeEach can set an
|
|
130
|
+
// env var, same reasoning as parsePositiveInt's own comment above. Both
|
|
131
|
+
// fields are now plain values (a number and a string, both parsed exactly
|
|
132
|
+
// once per call) because nothing downstream needs to defer past that one
|
|
133
|
+
// read: `max`'s only reason to be a function elsewhere in this file is so a
|
|
134
|
+
// caller can re-invoke it at its own use site, and nothing calls this
|
|
135
|
+
// builder's returned `max` more than once per registration either.
|
|
136
|
+
export function ipOnlyRateLimitConfig() {
|
|
137
|
+
return {
|
|
138
|
+
max: parsePositiveInt(process.env.VELORA_RATE_LIMIT_IP_MAX, 60),
|
|
139
|
+
// S162 second-review fix, preserved through the S165 reshape: this was
|
|
140
|
+
// once a bare `?? "1 minute"`, the exact bug parseWindow (above) exists
|
|
141
|
+
// to prevent. A blank VELORA_RATE_LIMIT_WINDOW would 500 every route in
|
|
142
|
+
// the setup scope (/api/setup/status, /api/setup/complete), the one
|
|
143
|
+
// boot path with no other way in on a fresh install.
|
|
144
|
+
timeWindow: parseWindow(process.env.VELORA_RATE_LIMIT_WINDOW, "1 minute"),
|
|
145
|
+
keyGenerator: ipOnlyKeyGenerator,
|
|
146
|
+
// Same error-response contract as auth/rate-limit.ts's rateLimitConfig
|
|
147
|
+
// (statusCode carried through so Fastify's default error handler
|
|
148
|
+
// answers 429 instead of 500 — see that file's comment) — duplicated
|
|
149
|
+
// rather than imported, for the same Prisma-avoidance reason as above.
|
|
150
|
+
errorResponseBuilder: (_request, context) => ({
|
|
151
|
+
statusCode: context.statusCode,
|
|
152
|
+
message: `Rate limit exceeded, retry in ${context.after}`,
|
|
153
|
+
}),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const ACCESS_TOKEN_TTL = "15m";
|
|
2
|
+
export declare const REFRESH_TOKEN_TTL_DAYS = 30;
|
|
3
|
+
export declare const REFRESH_COOKIE_NAME = "velora_refresh";
|
|
4
|
+
export declare function refreshCookieOptions(): {
|
|
5
|
+
httpOnly: true;
|
|
6
|
+
secure: boolean;
|
|
7
|
+
sameSite: "lax";
|
|
8
|
+
path: string;
|
|
9
|
+
maxAge: number;
|
|
10
|
+
};
|
|
11
|
+
export declare function hashPassword(password: string): Promise<string>;
|
|
12
|
+
export declare function verifyPassword(password: string, passwordHash: string): Promise<boolean>;
|
|
13
|
+
export declare function generateRefreshToken(): string;
|
|
14
|
+
export declare function hashRefreshToken(token: string): string;
|
|
15
|
+
export declare function refreshTokenExpiryDate(): Date;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { randomBytes, createHash } from "node:crypto";
|
|
2
|
+
import bcrypt from "bcrypt";
|
|
3
|
+
const BCRYPT_SALT_ROUNDS = 12;
|
|
4
|
+
export const ACCESS_TOKEN_TTL = "15m";
|
|
5
|
+
export const REFRESH_TOKEN_TTL_DAYS = 30;
|
|
6
|
+
export const REFRESH_COOKIE_NAME = "velora_refresh";
|
|
7
|
+
// httpOnly so the refresh token is never reachable from JS (removes the
|
|
8
|
+
// XSS-theft risk the old in-memory-only approach was accepting); scoped to
|
|
9
|
+
// the auth path only, since no other route needs it; sameSite: 'lax' is
|
|
10
|
+
// enough CSRF protection here — it isn't sent on cross-site POSTs, only on
|
|
11
|
+
// top-level GET navigations, and every route that reads it is a POST.
|
|
12
|
+
export function refreshCookieOptions() {
|
|
13
|
+
return {
|
|
14
|
+
httpOnly: true,
|
|
15
|
+
secure: process.env.NODE_ENV === "production",
|
|
16
|
+
sameSite: "lax",
|
|
17
|
+
path: "/api/auth",
|
|
18
|
+
maxAge: REFRESH_TOKEN_TTL_DAYS * 24 * 60 * 60,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function hashPassword(password) {
|
|
22
|
+
return bcrypt.hash(password, BCRYPT_SALT_ROUNDS);
|
|
23
|
+
}
|
|
24
|
+
export function verifyPassword(password, passwordHash) {
|
|
25
|
+
return bcrypt.compare(password, passwordHash);
|
|
26
|
+
}
|
|
27
|
+
// Refresh tokens are high-entropy random values, not user-chosen secrets,
|
|
28
|
+
// so a fast hash is appropriate here — unlike passwords, there's no
|
|
29
|
+
// dictionary/brute-force risk to slow down with bcrypt.
|
|
30
|
+
export function generateRefreshToken() {
|
|
31
|
+
return randomBytes(32).toString("hex");
|
|
32
|
+
}
|
|
33
|
+
export function hashRefreshToken(token) {
|
|
34
|
+
return createHash("sha256").update(token).digest("hex");
|
|
35
|
+
}
|
|
36
|
+
export function refreshTokenExpiryDate() {
|
|
37
|
+
return new Date(Date.now() + REFRESH_TOKEN_TTL_DAYS * 24 * 60 * 60 * 1000);
|
|
38
|
+
}
|
package/dist/bin.d.ts
ADDED
package/dist/bin.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The `velora-server` command: identical to `node dist/index.js` — index.js
|
|
3
|
+
// is the boot entry (env load → client self-heal → core migrations → mode
|
|
4
|
+
// → app). Kept as a wrapper so the boot file stays importable-free of
|
|
5
|
+
// shebang concerns.
|
|
6
|
+
import "./index.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { BootMode } from "./resolve-boot-mode.js";
|
|
2
|
+
export type CoreMigrationGate = {
|
|
3
|
+
action: "refuse";
|
|
4
|
+
message: string;
|
|
5
|
+
} | {
|
|
6
|
+
action: "log";
|
|
7
|
+
message: string;
|
|
8
|
+
} | {
|
|
9
|
+
action: "none";
|
|
10
|
+
};
|
|
11
|
+
export type CoreMigrationPhase = "client-generation" | "core-migrations";
|
|
12
|
+
export declare function judgeCoreMigrationFailure(error: unknown, mode: BootMode, phase: CoreMigrationPhase): CoreMigrationGate;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Two independent phrasings per phase, matched to how each already reads
|
|
2
|
+
// in its sentence: the refuse message says "Refusing to start: <refuse
|
|
3
|
+
// clause> against DATABASE_URL (…)"; the log message is its own sentence,
|
|
4
|
+
// "<log clause> (…) — continuing into setup." Keeping them as separate
|
|
5
|
+
// strings (rather than deriving one from the other) avoids grammar that
|
|
6
|
+
// only happens to work for one phase.
|
|
7
|
+
const PHASE_WORDING = {
|
|
8
|
+
"client-generation": {
|
|
9
|
+
refuseClause: "Prisma client generation failed",
|
|
10
|
+
logClause: "Prisma client generation did not succeed yet",
|
|
11
|
+
},
|
|
12
|
+
"core-migrations": {
|
|
13
|
+
refuseClause: "core migrations could not run",
|
|
14
|
+
logClause: "Core migrations did not run yet",
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
// The actual decision: given whatever error (if any) was captured while
|
|
18
|
+
// preparing the Prisma-owned schema, and the boot mode that was
|
|
19
|
+
// subsequently resolved, what should index.ts do about it?
|
|
20
|
+
//
|
|
21
|
+
// "full" means DATABASE_URL is configured AND an admin already exists —
|
|
22
|
+
// this is the one mode where a broken/un-migrated schema must not be
|
|
23
|
+
// allowed to boot silently, so it ALWAYS refuses on any error. This is the
|
|
24
|
+
// never-boot-un-migrated invariant, and core-migration-gate.test.ts pins
|
|
25
|
+
// it: no future edit here may make "full" tolerate an error, no matter
|
|
26
|
+
// what phase or error shape is added later.
|
|
27
|
+
//
|
|
28
|
+
// The setup-* modes never refuse — a missing table or missing database is
|
|
29
|
+
// the expected reason they were selected in the first place (see
|
|
30
|
+
// resolve-boot-mode.ts's own header) — they get at most one quiet log
|
|
31
|
+
// line, not a raw stack.
|
|
32
|
+
//
|
|
33
|
+
// No captured error at all (the normal case — self-heal and migrations
|
|
34
|
+
// both succeeded, or index.ts never even attempted them because
|
|
35
|
+
// DATABASE_URL was unset) means there is nothing to decide: "none".
|
|
36
|
+
export function judgeCoreMigrationFailure(error, mode, phase) {
|
|
37
|
+
if (error === undefined)
|
|
38
|
+
return { action: "none" };
|
|
39
|
+
const causeMessage = error instanceof Error ? error.message : String(error);
|
|
40
|
+
const wording = PHASE_WORDING[phase];
|
|
41
|
+
if (mode === "full") {
|
|
42
|
+
return {
|
|
43
|
+
action: "refuse",
|
|
44
|
+
message: `Refusing to start: ${wording.refuseClause} against DATABASE_URL (${causeMessage}). ` +
|
|
45
|
+
"Fix the database (or unset DATABASE_URL to re-enter setup) and start again.",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
action: "log",
|
|
50
|
+
message: `${wording.logClause} (${causeMessage}) — continuing into setup.`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type BootMode = "setup-full" | "setup-admin-only" | "full";
|
|
2
|
+
export declare const ADMIN_PROBE_TIMEOUT_MS = 10000;
|
|
3
|
+
export declare function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T>;
|
|
4
|
+
export declare function resolveBootMode(env: NodeJS.ProcessEnv, countAdmins: () => Promise<number>): Promise<BootMode>;
|
|
5
|
+
export declare function describeDatabaseUrlShape(url: string | undefined): string;
|
|
6
|
+
export declare function countAdminUsers(): Promise<number>;
|