@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.
Files changed (367) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +65 -0
  3. package/dist/admin-bundle/assets/__virtual_mf___mfe_internal__velora_admin__mf_owner__1__loadShare__react__loadShare__.js_commonjs-proxy-Ng0fOpdU.js +1 -0
  4. 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
  5. package/dist/admin-bundle/assets/_virtual_mf-localSharedImportMap___mfe_internal__velora_admin__mf_owner__1-DOKdWYyj.js +1 -0
  6. 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
  7. package/dist/admin-bundle/assets/_virtual_mf___mfe_internal__velora_admin__mf_owner__1__loadShare__react__loadShare__.js-BuUBskgj.js +9 -0
  8. 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
  9. 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
  10. 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
  11. package/dist/admin-bundle/assets/content-trash.schema-DfXHkC-w.js +72 -0
  12. package/dist/admin-bundle/assets/hostInit--C4xK6nj.js +1 -0
  13. package/dist/admin-bundle/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  14. package/dist/admin-bundle/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  15. package/dist/admin-bundle/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
  16. package/dist/admin-bundle/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
  17. package/dist/admin-bundle/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  18. package/dist/admin-bundle/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  19. package/dist/admin-bundle/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
  20. package/dist/admin-bundle/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
  21. package/dist/admin-bundle/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  22. package/dist/admin-bundle/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  23. package/dist/admin-bundle/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  24. package/dist/admin-bundle/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  25. package/dist/admin-bundle/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  26. package/dist/admin-bundle/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  27. package/dist/admin-bundle/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
  28. package/dist/admin-bundle/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
  29. package/dist/admin-bundle/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  30. package/dist/admin-bundle/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  31. package/dist/admin-bundle/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
  32. package/dist/admin-bundle/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
  33. package/dist/admin-bundle/assets/ibm-plex-sans-arabic-arabic-400-normal-CyU-ddYS.woff2 +0 -0
  34. package/dist/admin-bundle/assets/ibm-plex-sans-arabic-arabic-400-normal-EA1o-u_Q.woff +0 -0
  35. package/dist/admin-bundle/assets/ibm-plex-sans-arabic-arabic-500-normal-C4MQITzh.woff2 +0 -0
  36. package/dist/admin-bundle/assets/ibm-plex-sans-arabic-arabic-500-normal-DPXcppbm.woff +0 -0
  37. package/dist/admin-bundle/assets/ibm-plex-sans-arabic-arabic-600-normal-0pRdybE_.woff2 +0 -0
  38. package/dist/admin-bundle/assets/ibm-plex-sans-arabic-arabic-600-normal-MJZ0liF5.woff +0 -0
  39. package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-400-normal-Bo5KPYvw.woff2 +0 -0
  40. package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-400-normal-CFZh1unN.woff +0 -0
  41. package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-500-normal-BletK1Hp.woff +0 -0
  42. package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-500-normal-Cd6jVIg7.woff2 +0 -0
  43. package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-600-normal-Dk5eISD_.woff +0 -0
  44. package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-600-normal-KrqB56Mw.woff2 +0 -0
  45. package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-ext-400-normal-BeVLg08x.woff +0 -0
  46. package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-ext-400-normal-BpLfq-63.woff2 +0 -0
  47. package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-ext-500-normal-Ck_W4G78.woff2 +0 -0
  48. package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-ext-500-normal-eJxPnL1P.woff +0 -0
  49. package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-ext-600-normal-DT29Q8HW.woff +0 -0
  50. package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-ext-600-normal-DjDAeNQD.woff2 +0 -0
  51. package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-400-normal-BTotfTJu.woff +0 -0
  52. package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-400-normal-DZqxrq2p.woff2 +0 -0
  53. package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-500-normal-ByOcLdNv.woff +0 -0
  54. package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-500-normal-CocWQlwt.woff2 +0 -0
  55. package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-600-normal-71GNu3SW.woff2 +0 -0
  56. package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-600-normal-BGq0mW3O.woff +0 -0
  57. package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-ext-400-normal-Dsrv2Tcn.woff +0 -0
  58. package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-ext-400-normal-g30qAdWV.woff2 +0 -0
  59. package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-ext-500-normal-Cs5J6C77.woff2 +0 -0
  60. package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-ext-500-normal-DB5PtV2g.woff +0 -0
  61. package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-ext-600-normal-Bz0x94Yp.woff +0 -0
  62. package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-ext-600-normal-DUMzJB7m.woff2 +0 -0
  63. package/dist/admin-bundle/assets/ibm-plex-sans-greek-400-normal-D9ESIMu3.woff +0 -0
  64. package/dist/admin-bundle/assets/ibm-plex-sans-greek-400-normal-_efipK4i.woff2 +0 -0
  65. package/dist/admin-bundle/assets/ibm-plex-sans-greek-500-normal-CuWXN6rf.woff +0 -0
  66. package/dist/admin-bundle/assets/ibm-plex-sans-greek-500-normal-JMMifIXV.woff2 +0 -0
  67. package/dist/admin-bundle/assets/ibm-plex-sans-greek-600-normal-D-CqTdkO.woff +0 -0
  68. package/dist/admin-bundle/assets/ibm-plex-sans-greek-600-normal-DzTrcv_p.woff2 +0 -0
  69. package/dist/admin-bundle/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
  70. package/dist/admin-bundle/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
  71. package/dist/admin-bundle/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
  72. package/dist/admin-bundle/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
  73. package/dist/admin-bundle/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
  74. package/dist/admin-bundle/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
  75. package/dist/admin-bundle/assets/ibm-plex-sans-latin-ext-400-normal-C5H60-Va.woff2 +0 -0
  76. package/dist/admin-bundle/assets/ibm-plex-sans-latin-ext-400-normal-RBey6euL.woff +0 -0
  77. package/dist/admin-bundle/assets/ibm-plex-sans-latin-ext-500-normal-D0aIdm-b.woff +0 -0
  78. package/dist/admin-bundle/assets/ibm-plex-sans-latin-ext-500-normal-DakdToA3.woff2 +0 -0
  79. package/dist/admin-bundle/assets/ibm-plex-sans-latin-ext-600-normal-DIrixKbi.woff +0 -0
  80. package/dist/admin-bundle/assets/ibm-plex-sans-latin-ext-600-normal-DOrvGEcy.woff2 +0 -0
  81. package/dist/admin-bundle/assets/ibm-plex-sans-vietnamese-400-normal-DG4YqDda.woff2 +0 -0
  82. package/dist/admin-bundle/assets/ibm-plex-sans-vietnamese-400-normal-fK1oJ5dG.woff +0 -0
  83. package/dist/admin-bundle/assets/ibm-plex-sans-vietnamese-500-normal-BEb3_waV.woff +0 -0
  84. package/dist/admin-bundle/assets/ibm-plex-sans-vietnamese-500-normal-e4dixQRQ.woff2 +0 -0
  85. package/dist/admin-bundle/assets/ibm-plex-sans-vietnamese-600-normal-DgdngZtN.woff +0 -0
  86. package/dist/admin-bundle/assets/ibm-plex-sans-vietnamese-600-normal-DpPYBSTl.woff2 +0 -0
  87. package/dist/admin-bundle/assets/index-BIAF0ShP.js +542 -0
  88. package/dist/admin-bundle/assets/index-DhMY-0sC.js +148 -0
  89. package/dist/admin-bundle/assets/index-DnZV_gsg.css +1 -0
  90. package/dist/admin-bundle/assets/index-Do71v6NZ.js +7 -0
  91. package/dist/admin-bundle/assets/literata-cyrillic-ext-opsz-italic-5yUt4G_9.woff2 +0 -0
  92. package/dist/admin-bundle/assets/literata-cyrillic-ext-opsz-normal-C5tfwALZ.woff2 +0 -0
  93. package/dist/admin-bundle/assets/literata-cyrillic-opsz-italic-Bj0ndbyp.woff2 +0 -0
  94. package/dist/admin-bundle/assets/literata-cyrillic-opsz-normal-DgmypKXX.woff2 +0 -0
  95. package/dist/admin-bundle/assets/literata-greek-ext-opsz-italic-DFpO5j-Z.woff2 +0 -0
  96. package/dist/admin-bundle/assets/literata-greek-ext-opsz-normal-DGm64vGR.woff2 +0 -0
  97. package/dist/admin-bundle/assets/literata-greek-opsz-italic-CHERNRFq.woff2 +0 -0
  98. package/dist/admin-bundle/assets/literata-greek-opsz-normal-DFglUmAd.woff2 +0 -0
  99. package/dist/admin-bundle/assets/literata-latin-ext-opsz-italic-4y8iAqqs.woff2 +0 -0
  100. package/dist/admin-bundle/assets/literata-latin-ext-opsz-normal-STiTPs0F.woff2 +0 -0
  101. package/dist/admin-bundle/assets/literata-latin-opsz-italic-CJ93D38p.woff2 +0 -0
  102. package/dist/admin-bundle/assets/literata-latin-opsz-normal-BHZsWIlc.woff2 +0 -0
  103. package/dist/admin-bundle/assets/literata-vietnamese-opsz-italic-BdcrZMcO.woff2 +0 -0
  104. package/dist/admin-bundle/assets/literata-vietnamese-opsz-normal-Gqw5JQlq.woff2 +0 -0
  105. package/dist/admin-bundle/assets/mf-entry-bootstrap-0-aef015cd.js +41 -0
  106. package/dist/admin-bundle/assets/remoteEntry-CNyN68bc.js +14 -0
  107. package/dist/admin-bundle/assets/virtualExposes-BDCSo5OS.js +1 -0
  108. package/dist/admin-bundle/assets/vite-preload-helper-Cgledu0r.js +1 -0
  109. package/dist/admin-bundle/index.html +16 -0
  110. package/dist/app.d.ts +8 -0
  111. package/dist/app.js +252 -0
  112. package/dist/auth/api-content-author.d.ts +2 -0
  113. package/dist/auth/api-content-author.js +27 -0
  114. package/dist/auth/api-keys.d.ts +7 -0
  115. package/dist/auth/api-keys.js +85 -0
  116. package/dist/auth/jwt-payload.d.ts +16 -0
  117. package/dist/auth/jwt-payload.js +1 -0
  118. package/dist/auth/preview-tokens.d.ts +7 -0
  119. package/dist/auth/preview-tokens.js +76 -0
  120. package/dist/auth/rate-limit.d.ts +16 -0
  121. package/dist/auth/rate-limit.js +283 -0
  122. package/dist/auth/require-auth.d.ts +4 -0
  123. package/dist/auth/require-auth.js +28 -0
  124. package/dist/auth/route-rate-limits.d.ts +31 -0
  125. package/dist/auth/route-rate-limits.js +155 -0
  126. package/dist/auth/tokens.d.ts +15 -0
  127. package/dist/auth/tokens.js +38 -0
  128. package/dist/bin.d.ts +2 -0
  129. package/dist/bin.js +6 -0
  130. package/dist/boot/core-migration-gate.d.ts +12 -0
  131. package/dist/boot/core-migration-gate.js +52 -0
  132. package/dist/boot/resolve-boot-mode.d.ts +6 -0
  133. package/dist/boot/resolve-boot-mode.js +206 -0
  134. package/dist/bundled-templates/com.velora.blog/manifest.json +39 -0
  135. package/dist/bundled-templates/com.velora.blog/package.tar.gz +0 -0
  136. package/dist/bundled-templates/com.velora.blog/sample-content.json +14 -0
  137. package/dist/config/admin-ui.d.ts +1 -0
  138. package/dist/config/admin-ui.js +10 -0
  139. package/dist/config/cors.d.ts +7 -0
  140. package/dist/config/cors.js +53 -0
  141. package/dist/config/env-file-path.d.ts +2 -0
  142. package/dist/config/env-file-path.js +22 -0
  143. package/dist/config/graphql-limits.d.ts +2 -0
  144. package/dist/config/graphql-limits.js +26 -0
  145. package/dist/config/headless.d.ts +1 -0
  146. package/dist/config/headless.js +8 -0
  147. package/dist/config/license-revalidation.d.ts +2 -0
  148. package/dist/config/license-revalidation.js +16 -0
  149. package/dist/config/load-env.d.ts +1 -0
  150. package/dist/config/load-env.js +53 -0
  151. package/dist/config/marketplace.d.ts +11 -0
  152. package/dist/config/marketplace.js +19 -0
  153. package/dist/config/preview.d.ts +3 -0
  154. package/dist/config/preview.js +39 -0
  155. package/dist/config/public-docs.d.ts +1 -0
  156. package/dist/config/public-docs.js +9 -0
  157. package/dist/config/site.d.ts +7 -0
  158. package/dist/config/site.js +12 -0
  159. package/dist/config/trust-proxy.d.ts +2 -0
  160. package/dist/config/trust-proxy.js +42 -0
  161. package/dist/data-types/validate-data-type-settings.d.ts +1 -0
  162. package/dist/data-types/validate-data-type-settings.js +33 -0
  163. package/dist/db/content-db.d.ts +16 -0
  164. package/dist/db/content-db.js +109 -0
  165. package/dist/db/core-migrations/index.d.ts +14 -0
  166. package/dist/db/core-migrations/index.js +184 -0
  167. package/dist/db/core-migrations/steps.d.ts +4 -0
  168. package/dist/db/core-migrations/steps.js +66 -0
  169. package/dist/db/load-prisma-client.d.ts +2 -0
  170. package/dist/db/load-prisma-client.js +44 -0
  171. package/dist/db/prisma.d.ts +3 -0
  172. package/dist/db/prisma.js +67 -0
  173. package/dist/db/resolve-client-dir.d.ts +2 -0
  174. package/dist/db/resolve-client-dir.js +83 -0
  175. package/dist/docs/filter-public-api-spec.d.ts +2 -0
  176. package/dist/docs/filter-public-api-spec.js +52 -0
  177. package/dist/docs-bundle/404.html +41 -0
  178. package/dist/docs-bundle/_astro/MobileTableOfContents.astro_astro_type_script_index_0_lang.BcSo_yiZ.js +1 -0
  179. package/dist/docs-bundle/_astro/Search.astro_astro_type_script_index_0_lang.CavldbAs.js +1 -0
  180. package/dist/docs-bundle/_astro/TableOfContents.astro_astro_type_script_index_0_lang.Csloo1VZ.js +1 -0
  181. package/dist/docs-bundle/_astro/common.DTXA7sXM.css +3 -0
  182. package/dist/docs-bundle/_astro/ec.0vx5m.js +3 -0
  183. package/dist/docs-bundle/_astro/ec.w36nc.css +1 -0
  184. package/dist/docs-bundle/_astro/page.Dwipeu-R.js +1 -0
  185. package/dist/docs-bundle/_astro/print.ehPL0gv-.css +1 -0
  186. package/dist/docs-bundle/_astro/starlight-toc.CTCQXEge.js +1 -0
  187. package/dist/docs-bundle/_astro/ui-core.D-7Scnhs.js +2 -0
  188. package/dist/docs-bundle/favicon.svg +1 -0
  189. package/dist/docs-bundle/index.html +1 -0
  190. package/dist/docs-bundle/pagefind/fragment/en_18a36d7.pf_fragment +0 -0
  191. package/dist/docs-bundle/pagefind/fragment/en_1d9eb24.pf_fragment +0 -0
  192. package/dist/docs-bundle/pagefind/fragment/en_1ef320c.pf_fragment +0 -0
  193. package/dist/docs-bundle/pagefind/fragment/en_3099406.pf_fragment +0 -0
  194. package/dist/docs-bundle/pagefind/fragment/en_4a5b6fe.pf_fragment +0 -0
  195. package/dist/docs-bundle/pagefind/fragment/en_9aa652e.pf_fragment +0 -0
  196. package/dist/docs-bundle/pagefind/fragment/en_baccdf6.pf_fragment +0 -0
  197. package/dist/docs-bundle/pagefind/fragment/en_d81f3fe.pf_fragment +0 -0
  198. package/dist/docs-bundle/pagefind/fragment/en_d855550.pf_fragment +0 -0
  199. package/dist/docs-bundle/pagefind/fragment/en_e6d88d8.pf_fragment +0 -0
  200. package/dist/docs-bundle/pagefind/fragment/en_e9d3189.pf_fragment +0 -0
  201. package/dist/docs-bundle/pagefind/index/en_5feeed7.pf_index +0 -0
  202. package/dist/docs-bundle/pagefind/index/en_c6981f9.pf_index +0 -0
  203. package/dist/docs-bundle/pagefind/index/en_ce4f6bf.pf_index +0 -0
  204. package/dist/docs-bundle/pagefind/pagefind-component-ui.css +1509 -0
  205. package/dist/docs-bundle/pagefind/pagefind-component-ui.js +56 -0
  206. package/dist/docs-bundle/pagefind/pagefind-entry.json +1 -0
  207. package/dist/docs-bundle/pagefind/pagefind-highlight.js +1070 -0
  208. package/dist/docs-bundle/pagefind/pagefind-modular-ui.css +214 -0
  209. package/dist/docs-bundle/pagefind/pagefind-modular-ui.js +8 -0
  210. package/dist/docs-bundle/pagefind/pagefind-ui.css +1 -0
  211. package/dist/docs-bundle/pagefind/pagefind-ui.js +2 -0
  212. package/dist/docs-bundle/pagefind/pagefind-worker.js +6 -0
  213. package/dist/docs-bundle/pagefind/pagefind.en_1e94abd852.pf_meta +0 -0
  214. package/dist/docs-bundle/pagefind/pagefind.js +6 -0
  215. package/dist/docs-bundle/pagefind/wasm.en.pagefind +0 -0
  216. package/dist/docs-bundle/pagefind/wasm.unknown.pagefind +0 -0
  217. package/dist/docs-bundle/v1/index.html +86 -0
  218. package/dist/docs-bundle/v1/using-velora/build-a-headless-frontend/index.html +413 -0
  219. package/dist/docs-bundle/v1/using-velora/content-model/index.html +858 -0
  220. package/dist/docs-bundle/v1/using-velora/editing/index.html +748 -0
  221. package/dist/docs-bundle/v1/using-velora/getting-started/index.html +588 -0
  222. package/dist/docs-bundle/v1/using-velora/headless-api/index.html +1620 -0
  223. package/dist/docs-bundle/v1/using-velora/index.html +215 -0
  224. package/dist/docs-bundle/v1/using-velora/installation/index.html +886 -0
  225. package/dist/docs-bundle/v1/using-velora/installing-plugins/index.html +836 -0
  226. package/dist/docs-bundle/v1/using-velora/media/index.html +468 -0
  227. package/dist/docs-bundle/v1/using-velora/users-and-roles/index.html +561 -0
  228. package/dist/graphql/field-count-rule.d.ts +5 -0
  229. package/dist/graphql/field-count-rule.js +59 -0
  230. package/dist/graphql/schema.d.ts +12 -0
  231. package/dist/graphql/schema.js +330 -0
  232. package/dist/index.d.ts +1 -0
  233. package/dist/index.js +180 -0
  234. package/dist/lib.d.ts +2 -0
  235. package/dist/lib.js +39 -0
  236. package/dist/marketplace/install-bundle-members.d.ts +33 -0
  237. package/dist/marketplace/install-bundle-members.js +137 -0
  238. package/dist/marketplace/install-from-marketplace.d.ts +60 -0
  239. package/dist/marketplace/install-from-marketplace.js +581 -0
  240. package/dist/marketplace/license-revalidation-job.d.ts +38 -0
  241. package/dist/marketplace/license-revalidation-job.js +139 -0
  242. package/dist/marketplace/marketplace-client.d.ts +43 -0
  243. package/dist/marketplace/marketplace-client.js +228 -0
  244. package/dist/marketplace/revalidate-license.d.ts +39 -0
  245. package/dist/marketplace/revalidate-license.js +103 -0
  246. package/dist/media/media-service.d.ts +36 -0
  247. package/dist/media/media-service.js +162 -0
  248. package/dist/plugins/boot-plugin-loader.d.ts +32 -0
  249. package/dist/plugins/boot-plugin-loader.js +57 -0
  250. package/dist/plugins/hook-dispatcher.d.ts +13 -0
  251. package/dist/plugins/hook-dispatcher.js +68 -0
  252. package/dist/plugins/install-built-package.d.ts +36 -0
  253. package/dist/plugins/install-built-package.js +199 -0
  254. package/dist/plugins/install-package-tarball.d.ts +25 -0
  255. package/dist/plugins/install-package-tarball.js +73 -0
  256. package/dist/plugins/plugin-context.d.ts +5 -0
  257. package/dist/plugins/plugin-context.js +26 -0
  258. package/dist/plugins/plugin-package-store.d.ts +31 -0
  259. package/dist/plugins/plugin-package-store.js +64 -0
  260. package/dist/plugins/plugin-registry-service.d.ts +227 -0
  261. package/dist/plugins/plugin-registry-service.js +636 -0
  262. package/dist/plugins/seed-template-content.d.ts +23 -0
  263. package/dist/plugins/seed-template-content.js +198 -0
  264. package/dist/plugins/server-serializers.d.ts +3 -0
  265. package/dist/plugins/server-serializers.js +16 -0
  266. package/dist/plugins/simple-storage-api.d.ts +9 -0
  267. package/dist/plugins/simple-storage-api.js +26 -0
  268. package/dist/plugins/snapshot-groups.d.ts +16 -0
  269. package/dist/plugins/snapshot-groups.js +81 -0
  270. package/dist/plugins/structured-query-builder.d.ts +21 -0
  271. package/dist/plugins/structured-query-builder.js +60 -0
  272. package/dist/plugins/structured-snapshot-service.d.ts +16 -0
  273. package/dist/plugins/structured-snapshot-service.js +286 -0
  274. package/dist/plugins/structured-storage-api.d.ts +22 -0
  275. package/dist/plugins/structured-storage-api.js +91 -0
  276. package/dist/plugins/structured-storage-transaction.d.ts +22 -0
  277. package/dist/plugins/structured-storage-transaction.js +63 -0
  278. package/dist/plugins/structured-table-naming.d.ts +1 -0
  279. package/dist/plugins/structured-table-naming.js +18 -0
  280. package/dist/plugins/trusted-keys.d.ts +11 -0
  281. package/dist/plugins/trusted-keys.js +67 -0
  282. package/dist/plugins/trusted-signing-keys.d.ts +27 -0
  283. package/dist/plugins/trusted-signing-keys.js +40 -0
  284. package/dist/plugins/uninstall-bundle.d.ts +29 -0
  285. package/dist/plugins/uninstall-bundle.js +150 -0
  286. package/dist/plugins/verify-package.d.ts +36 -0
  287. package/dist/plugins/verify-package.js +121 -0
  288. package/dist/routes/admin-static.d.ts +8 -0
  289. package/dist/routes/admin-static.js +196 -0
  290. package/dist/routes/api-keys.d.ts +2 -0
  291. package/dist/routes/api-keys.js +95 -0
  292. package/dist/routes/api-playground.d.ts +2 -0
  293. package/dist/routes/api-playground.js +21 -0
  294. package/dist/routes/auth.d.ts +2 -0
  295. package/dist/routes/auth.js +102 -0
  296. package/dist/routes/content.d.ts +3 -0
  297. package/dist/routes/content.js +471 -0
  298. package/dist/routes/data-types.d.ts +3 -0
  299. package/dist/routes/data-types.js +100 -0
  300. package/dist/routes/docs-static.d.ts +8 -0
  301. package/dist/routes/docs-static.js +137 -0
  302. package/dist/routes/document-types.d.ts +3 -0
  303. package/dist/routes/document-types.js +149 -0
  304. package/dist/routes/headless-bridge.d.ts +10 -0
  305. package/dist/routes/headless-bridge.js +207 -0
  306. package/dist/routes/health.d.ts +2 -0
  307. package/dist/routes/health.js +15 -0
  308. package/dist/routes/marketplace-proxy.d.ts +8 -0
  309. package/dist/routes/marketplace-proxy.js +135 -0
  310. package/dist/routes/media.d.ts +6 -0
  311. package/dist/routes/media.js +93 -0
  312. package/dist/routes/plugin-api-routes.d.ts +12 -0
  313. package/dist/routes/plugin-api-routes.js +224 -0
  314. package/dist/routes/plugin-assets.d.ts +8 -0
  315. package/dist/routes/plugin-assets.js +51 -0
  316. package/dist/routes/plugin-management.d.ts +7 -0
  317. package/dist/routes/plugin-management.js +525 -0
  318. package/dist/routes/plugin-storage.d.ts +8 -0
  319. package/dist/routes/plugin-storage.js +104 -0
  320. package/dist/routes/plugins.d.ts +2 -0
  321. package/dist/routes/plugins.js +36 -0
  322. package/dist/routes/public-api.d.ts +9 -0
  323. package/dist/routes/public-api.js +545 -0
  324. package/dist/routes/published-item.d.ts +57 -0
  325. package/dist/routes/published-item.js +236 -0
  326. package/dist/routes/setup.d.ts +9 -0
  327. package/dist/routes/setup.js +477 -0
  328. package/dist/routes/snapshots.d.ts +3 -0
  329. package/dist/routes/snapshots.js +65 -0
  330. package/dist/routes/templates.d.ts +3 -0
  331. package/dist/routes/templates.js +37 -0
  332. package/dist/routes/themes.d.ts +2 -0
  333. package/dist/routes/themes.js +30 -0
  334. package/dist/routes/users.d.ts +2 -0
  335. package/dist/routes/users.js +113 -0
  336. package/dist/sandbox/register-local-plugin.d.ts +36 -0
  337. package/dist/sandbox/register-local-plugin.js +108 -0
  338. package/dist/sandbox/seed-sandbox.d.ts +20 -0
  339. package/dist/sandbox/seed-sandbox.js +122 -0
  340. package/dist/scripts/copy-admin-bundle.d.ts +1 -0
  341. package/dist/scripts/copy-admin-bundle.js +24 -0
  342. package/dist/scripts/copy-docs-bundle.d.ts +1 -0
  343. package/dist/scripts/copy-docs-bundle.js +22 -0
  344. package/dist/scripts/create-api-key.d.ts +5 -0
  345. package/dist/scripts/create-api-key.js +56 -0
  346. package/dist/scripts/generate-prisma-clients.d.ts +6 -0
  347. package/dist/scripts/generate-prisma-clients.js +125 -0
  348. package/dist/scripts/pack-bundled-templates.d.ts +1 -0
  349. package/dist/scripts/pack-bundled-templates.js +87 -0
  350. package/dist/setup/build-connection-string.d.ts +19 -0
  351. package/dist/setup/build-connection-string.js +14 -0
  352. package/dist/setup/bundled-templates.d.ts +19 -0
  353. package/dist/setup/bundled-templates.js +121 -0
  354. package/dist/setup/ensure-prisma-client.d.ts +3 -0
  355. package/dist/setup/ensure-prisma-client.js +23 -0
  356. package/dist/setup/install-starter.d.ts +14 -0
  357. package/dist/setup/install-starter.js +76 -0
  358. package/dist/setup/normalize-database-url.d.ts +2 -0
  359. package/dist/setup/normalize-database-url.js +35 -0
  360. package/dist/setup/persist-env-config.d.ts +2 -0
  361. package/dist/setup/persist-env-config.js +63 -0
  362. package/dist/setup/provision-database.d.ts +2 -0
  363. package/dist/setup/provision-database.js +89 -0
  364. package/dist/setup-app.d.ts +4 -0
  365. package/dist/setup-app.js +43 -0
  366. package/package.json +96 -0
  367. 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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "./index.js";
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>;