@velora-cms/server 0.11.0 → 0.12.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/dist/admin/sync-admin-bundle.d.ts +9 -0
- package/dist/admin/sync-admin-bundle.js +120 -0
- package/dist/admin-bundle/.velora-version +1 -0
- package/dist/admin-bundle/assets/{_virtual_mf-localSharedImportMap___mfe_internal__velora_admin__mf_owner__1-DOKdWYyj.js → _virtual_mf-localSharedImportMap___mfe_internal__velora_admin__mf_owner__1-DlIgIuFP.js} +1 -1
- 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 → _virtual_mf___mfe_internal__velora_admin__mf_owner__1__loadShare___mf_0_velora_mf_2_cms_mf_1_plugin_mf_2_sdk__loadShare__.js-Bm9EahCG.js} +2 -2
- package/dist/admin-bundle/assets/{content-trash.schema-DfXHkC-w.js → content-trash.schema-tvJ4Lbos.js} +5 -5
- package/dist/admin-bundle/assets/{hostInit--C4xK6nj.js → hostInit-B-6-34XY.js} +1 -1
- package/dist/admin-bundle/assets/{index-DhMY-0sC.js → index-CHCGuSbb.js} +1 -1
- package/dist/admin-bundle/assets/{index-BIAF0ShP.js → index-DYO7-IK8.js} +33 -33
- package/dist/admin-bundle/assets/{mf-entry-bootstrap-0-aef015cd.js → mf-entry-bootstrap-0-7b0ce2fe.js} +2 -2
- package/dist/admin-bundle/assets/{remoteEntry-CNyN68bc.js → remoteEntry-J2yJXsGB.js} +2 -2
- package/dist/admin-bundle/index.html +4 -4
- package/dist/app.d.ts +16 -1
- package/dist/app.js +63 -4
- package/dist/auth/api-keys.d.ts +1 -0
- package/dist/auth/api-keys.js +5 -1
- package/dist/auth/route-rate-limits.d.ts +4 -0
- package/dist/auth/route-rate-limits.js +13 -0
- package/dist/bin-sync-admin.d.ts +2 -0
- package/dist/bin-sync-admin.js +39 -0
- package/dist/bundled-templates/com.velora.blog/package.tar.gz +0 -0
- package/dist/config/public-url.d.ts +3 -0
- package/dist/config/public-url.js +45 -0
- package/dist/db/core-migrations/steps.js +95 -16
- package/dist/docs-bundle/pagefind/fragment/en_42f5356.pf_fragment +0 -0
- package/dist/docs-bundle/pagefind/fragment/en_67dccf8.pf_fragment +0 -0
- package/dist/docs-bundle/pagefind/fragment/en_80941b8.pf_fragment +0 -0
- package/dist/docs-bundle/pagefind/fragment/en_a64440b.pf_fragment +0 -0
- package/dist/docs-bundle/pagefind/fragment/en_eb168aa.pf_fragment +0 -0
- package/dist/docs-bundle/pagefind/index/en_8450ec7.pf_index +0 -0
- package/dist/docs-bundle/pagefind/index/en_c285f7b.pf_index +0 -0
- package/dist/docs-bundle/pagefind/index/en_e8cada7.pf_index +0 -0
- package/dist/docs-bundle/pagefind/pagefind-entry.json +1 -1
- package/dist/docs-bundle/pagefind/pagefind.en_57d2f8922f.pf_meta +0 -0
- package/dist/docs-bundle/v1/using-velora/getting-started/index.html +5 -3
- package/dist/docs-bundle/v1/using-velora/headless-api/index.html +10 -4
- package/dist/docs-bundle/v1/using-velora/index.html +6 -4
- package/dist/docs-bundle/v1/using-velora/installation/index.html +73 -15
- package/dist/docs-bundle/v1/using-velora/media/index.html +118 -34
- package/dist/graphql/schema.d.ts +1 -0
- package/dist/graphql/schema.js +5 -2
- package/dist/lib.d.ts +1 -0
- package/dist/lib.js +45 -11
- package/dist/media/media-service.d.ts +14 -15
- package/dist/media/media-service.js +121 -59
- package/dist/media/media-tokens.d.ts +6 -0
- package/dist/media/media-tokens.js +88 -0
- package/dist/media/storage/local.d.ts +16 -0
- package/dist/media/storage/local.js +99 -0
- package/dist/media/storage/s3.d.ts +25 -0
- package/dist/media/storage/s3.js +134 -0
- package/dist/media/storage/types.d.ts +21 -0
- package/dist/media/storage/types.js +44 -0
- package/dist/routes/admin-static.d.ts +2 -0
- package/dist/routes/admin-static.js +42 -4
- package/dist/routes/media-public.d.ts +6 -0
- package/dist/routes/media-public.js +265 -0
- package/dist/routes/media.d.ts +2 -1
- package/dist/routes/media.js +95 -5
- package/dist/routes/plugin-storage.js +5 -1
- package/dist/routes/public-api.js +5 -3
- package/dist/routes/published-item.d.ts +2 -1
- package/dist/routes/published-item.js +28 -5
- package/dist/scripts/copy-admin-bundle.js +9 -1
- package/package.json +7 -6
- package/prisma/schema.prisma +16 -3
- package/dist/docs-bundle/pagefind/fragment/en_1d9eb24.pf_fragment +0 -0
- package/dist/docs-bundle/pagefind/fragment/en_3099406.pf_fragment +0 -0
- package/dist/docs-bundle/pagefind/fragment/en_9aa652e.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/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.en_1e94abd852.pf_meta +0 -0
|
@@ -70,11 +70,14 @@
|
|
|
70
70
|
})();
|
|
71
71
|
</script></sl-sidebar-state-persist><div class="md:sl-hidden"><div class="mobile-preferences sl-flex astro-yfet6llj"><div class="social-icons astro-yfet6llj"></div><starlight-theme-select><label style="--sl-select-width: 6.25em" class="astro-7uz3f5wr"><span class="sr-only astro-7uz3f5wr">Select theme</span><svg aria-hidden="true" class="icon label-icon astro-7uz3f5wr astro-24n5ceuf" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M21 14h-1V7a3 3 0 0 0-3-3H7a3 3 0 0 0-3 3v7H3a1 1 0 0 0-1 1v2a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3v-2a1 1 0 0 0-1-1ZM6 7a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v7H6V7Zm14 10a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-1h16v1Z"/></svg><select autocomplete="off" class="astro-7uz3f5wr"><option value="dark" class="astro-7uz3f5wr">Dark</option><option value="light" class="astro-7uz3f5wr">Light</option><option value="auto" selected class="astro-7uz3f5wr">Auto</option></select><svg aria-hidden="true" class="icon caret astro-7uz3f5wr astro-24n5ceuf" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M17 9.17a1 1 0 0 0-1.41 0L12 12.71 8.46 9.17a1 1 0 1 0-1.41 1.42l4.24 4.24a1.002 1.002 0 0 0 1.42 0L17 10.59a1.002 1.002 0 0 0 0-1.42Z"/></svg></label></starlight-theme-select><script>
|
|
72
72
|
StarlightThemeProvider.updatePickers();
|
|
73
|
-
</script></div></div></div></div></nav><div class="main-frame astro-5auw3bkx"><script type="module">var e=document.getElementById(`starlight__sidebar`),t=e?.querySelector(`sl-sidebar-state-persist`),n=`sl-sidebar-state`,r=()=>{let r=[],i=t?.dataset.hash||``;try{let e=sessionStorage.getItem(n),t=JSON.parse(e||`{}`);Array.isArray(t.open)&&t.hash===i&&(r=t.open)}catch{}return{hash:i,open:r,scroll:e?.scrollTop||0}},i=e=>{try{sessionStorage.setItem(n,JSON.stringify(e))}catch{}},a=()=>i(r()),o=(e,t)=>{let n=r();n.open[t]=e,i(n)};t?.addEventListener(`click`,e=>{if(!(e.target instanceof Element))return;let t=e.target.closest(`summary`)?.closest(`details`);if(!t)return;let n=t.querySelector(`sl-sidebar-restore`),r=parseInt(n?.dataset.index||``);isNaN(r)||o(!t.open,r)}),addEventListener(`visibilitychange`,()=>{document.visibilityState===`hidden`&&a()});</script><div class="lg:sl-flex astro-perbdhd2"><aside class="right-sidebar-container print:hidden astro-perbdhd2"><div class="right-sidebar astro-perbdhd2"><div class="lg:sl-hidden astro-epm5tkrc"><mobile-starlight-toc data-min-h="2" data-max-h="3" class="astro-bnvebtr4"><nav aria-labelledby="starlight__on-this-page--mobile" class="astro-bnvebtr4"><details id="starlight__mobile-toc" class="astro-bnvebtr4"><summary id="starlight__on-this-page--mobile" class="sl-flex astro-bnvebtr4"><span class="toggle sl-flex astro-bnvebtr4">On this page<svg aria-hidden="true" class="caret astro-bnvebtr4 astro-24n5ceuf" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1rem;"><path d="m14.83 11.29-4.24-4.24a1 1 0 1 0-1.42 1.41L12.71 12l-3.54 3.54a1 1 0 0 0 0 1.41 1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.24-4.24a1.002 1.002 0 0 0 0-1.42Z"/></svg></span><span class="display-current astro-bnvebtr4"></span></summary><div class="dropdown astro-bnvebtr4"><ul class="isMobile astro-lptfoybu" style="--depth: 0;"><li style="--depth: 0;" class="astro-lptfoybu"><a href="#_top" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Overview</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#the-media-library" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">The media library</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#what-a-media-record-stores" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">What a media record stores</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#where-the-bytes-live" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Where the bytes live</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#limits-and-what-is-not-checked" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Limits, and what is not checked</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#using-media-in-content" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Using media in content</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#the-api" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">The API</span></a><ul class="isMobile astro-lptfoybu" style="--depth: 1;"><li style="--depth: 1;" class="astro-lptfoybu"><a href="#upload-a-file" style="--depth: 1;" class="astro-lptfoybu"><span style="--depth: 1;" class="astro-lptfoybu">Upload a file</span></a></li><li style="--depth: 1;" class="astro-lptfoybu"><a href="#browse-the-library" style="--depth: 1;" class="astro-lptfoybu"><span style="--depth: 1;" class="astro-lptfoybu">Browse the library</span></a></li><li style="--depth: 1;" class="astro-lptfoybu"><a href="#delete-a-file" style="--depth: 1;" class="astro-lptfoybu"><span style="--depth: 1;" class="astro-lptfoybu">Delete a file</span></a></li><li style="--depth: 1;" class="astro-lptfoybu"><a href="#reading-media-from-a-frontend" style="--depth: 1;" class="astro-lptfoybu"><span style="--depth: 1;" class="astro-lptfoybu">Reading media from a frontend</span></a></li></ul></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#next" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Next</span></a></li></ul></div></details></nav></mobile-starlight-toc><script type="module" src="/docs/_astro/MobileTableOfContents.astro_astro_type_script_index_0_lang.BcSo_yiZ.js"></script></div><div class="right-sidebar-panel sl-hidden lg:sl-block astro-epm5tkrc"><div class="sl-container astro-epm5tkrc"><starlight-toc data-min-h="2" data-max-h="3"><nav aria-labelledby="starlight__on-this-page"><h2 id="starlight__on-this-page">On this page</h2><ul class="astro-lptfoybu" style="--depth: 0;"><li style="--depth: 0;" class="astro-lptfoybu"><a href="#_top" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Overview</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#the-media-library" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">The media library</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#what-a-media-record-stores" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">What a media record stores</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#where-the-bytes-live" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Where the bytes live</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#limits-and-what-is-not-checked" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Limits, and what is not checked</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#using-media-in-content" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Using media in content</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#the-api" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">The API</span></a><ul class="astro-lptfoybu" style="--depth: 1;"><li style="--depth: 1;" class="astro-lptfoybu"><a href="#upload-a-file" style="--depth: 1;" class="astro-lptfoybu"><span style="--depth: 1;" class="astro-lptfoybu">Upload a file</span></a></li><li style="--depth: 1;" class="astro-lptfoybu"><a href="#browse-the-library" style="--depth: 1;" class="astro-lptfoybu"><span style="--depth: 1;" class="astro-lptfoybu">Browse the library</span></a></li><li style="--depth: 1;" class="astro-lptfoybu"><a href="#delete-a-file" style="--depth: 1;" class="astro-lptfoybu"><span style="--depth: 1;" class="astro-lptfoybu">Delete a file</span></a></li><li style="--depth: 1;" class="astro-lptfoybu"><a href="#reading-media-from-a-frontend" style="--depth: 1;" class="astro-lptfoybu"><span style="--depth: 1;" class="astro-lptfoybu">Reading media from a frontend</span></a></li></ul></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#next" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Next</span></a></li></ul></nav></starlight-toc><script type="module" src="/docs/_astro/TableOfContents.astro_astro_type_script_index_0_lang.Csloo1VZ.js"></script></div></div></div></aside><div class="main-pane astro-perbdhd2"><main data-pagefind-body class="astro-ewhz5xxr" lang="en" dir="ltr"><div class="content-panel astro-ahkzlqop"><div class="sl-container astro-ahkzlqop"><h1 id="_top" class="astro-oef2efwn">Media</h1></div></div><div class="content-panel astro-ahkzlqop"><div class="sl-container astro-ahkzlqop"><div class="sl-markdown-content"><p>Velora splits every uploaded file in two: the <strong>bytes</strong> go to
|
|
74
|
-
S3-compatible object store, and a
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
</script></div></div></div></div></nav><div class="main-frame astro-5auw3bkx"><script type="module">var e=document.getElementById(`starlight__sidebar`),t=e?.querySelector(`sl-sidebar-state-persist`),n=`sl-sidebar-state`,r=()=>{let r=[],i=t?.dataset.hash||``;try{let e=sessionStorage.getItem(n),t=JSON.parse(e||`{}`);Array.isArray(t.open)&&t.hash===i&&(r=t.open)}catch{}return{hash:i,open:r,scroll:e?.scrollTop||0}},i=e=>{try{sessionStorage.setItem(n,JSON.stringify(e))}catch{}},a=()=>i(r()),o=(e,t)=>{let n=r();n.open[t]=e,i(n)};t?.addEventListener(`click`,e=>{if(!(e.target instanceof Element))return;let t=e.target.closest(`summary`)?.closest(`details`);if(!t)return;let n=t.querySelector(`sl-sidebar-restore`),r=parseInt(n?.dataset.index||``);isNaN(r)||o(!t.open,r)}),addEventListener(`visibilitychange`,()=>{document.visibilityState===`hidden`&&a()});</script><div class="lg:sl-flex astro-perbdhd2"><aside class="right-sidebar-container print:hidden astro-perbdhd2"><div class="right-sidebar astro-perbdhd2"><div class="lg:sl-hidden astro-epm5tkrc"><mobile-starlight-toc data-min-h="2" data-max-h="3" class="astro-bnvebtr4"><nav aria-labelledby="starlight__on-this-page--mobile" class="astro-bnvebtr4"><details id="starlight__mobile-toc" class="astro-bnvebtr4"><summary id="starlight__on-this-page--mobile" class="sl-flex astro-bnvebtr4"><span class="toggle sl-flex astro-bnvebtr4">On this page<svg aria-hidden="true" class="caret astro-bnvebtr4 astro-24n5ceuf" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1rem;"><path d="m14.83 11.29-4.24-4.24a1 1 0 1 0-1.42 1.41L12.71 12l-3.54 3.54a1 1 0 0 0 0 1.41 1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.24-4.24a1.002 1.002 0 0 0 0-1.42Z"/></svg></span><span class="display-current astro-bnvebtr4"></span></summary><div class="dropdown astro-bnvebtr4"><ul class="isMobile astro-lptfoybu" style="--depth: 0;"><li style="--depth: 0;" class="astro-lptfoybu"><a href="#_top" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Overview</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#the-media-library" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">The media library</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#what-a-media-record-stores" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">What a media record stores</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#where-the-bytes-live" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Where the bytes live</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#visibility-public-and-private" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Visibility: public and private</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#temporary-links-for-private-files" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Temporary links for private files</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#renditions-reserved" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Renditions (reserved)</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#limits-and-what-is-not-checked" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Limits, and what is not checked</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#using-media-in-content" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Using media in content</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#the-api" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">The API</span></a><ul class="isMobile astro-lptfoybu" style="--depth: 1;"><li style="--depth: 1;" class="astro-lptfoybu"><a href="#upload-a-file" style="--depth: 1;" class="astro-lptfoybu"><span style="--depth: 1;" class="astro-lptfoybu">Upload a file</span></a></li><li style="--depth: 1;" class="astro-lptfoybu"><a href="#browse-the-library" style="--depth: 1;" class="astro-lptfoybu"><span style="--depth: 1;" class="astro-lptfoybu">Browse the library</span></a></li><li style="--depth: 1;" class="astro-lptfoybu"><a href="#delete-a-file" style="--depth: 1;" class="astro-lptfoybu"><span style="--depth: 1;" class="astro-lptfoybu">Delete a file</span></a></li><li style="--depth: 1;" class="astro-lptfoybu"><a href="#reading-media-from-a-frontend" style="--depth: 1;" class="astro-lptfoybu"><span style="--depth: 1;" class="astro-lptfoybu">Reading media from a frontend</span></a></li></ul></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#next" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Next</span></a></li></ul></div></details></nav></mobile-starlight-toc><script type="module" src="/docs/_astro/MobileTableOfContents.astro_astro_type_script_index_0_lang.BcSo_yiZ.js"></script></div><div class="right-sidebar-panel sl-hidden lg:sl-block astro-epm5tkrc"><div class="sl-container astro-epm5tkrc"><starlight-toc data-min-h="2" data-max-h="3"><nav aria-labelledby="starlight__on-this-page"><h2 id="starlight__on-this-page">On this page</h2><ul class="astro-lptfoybu" style="--depth: 0;"><li style="--depth: 0;" class="astro-lptfoybu"><a href="#_top" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Overview</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#the-media-library" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">The media library</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#what-a-media-record-stores" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">What a media record stores</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#where-the-bytes-live" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Where the bytes live</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#visibility-public-and-private" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Visibility: public and private</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#temporary-links-for-private-files" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Temporary links for private files</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#renditions-reserved" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Renditions (reserved)</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#limits-and-what-is-not-checked" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Limits, and what is not checked</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#using-media-in-content" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Using media in content</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#the-api" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">The API</span></a><ul class="astro-lptfoybu" style="--depth: 1;"><li style="--depth: 1;" class="astro-lptfoybu"><a href="#upload-a-file" style="--depth: 1;" class="astro-lptfoybu"><span style="--depth: 1;" class="astro-lptfoybu">Upload a file</span></a></li><li style="--depth: 1;" class="astro-lptfoybu"><a href="#browse-the-library" style="--depth: 1;" class="astro-lptfoybu"><span style="--depth: 1;" class="astro-lptfoybu">Browse the library</span></a></li><li style="--depth: 1;" class="astro-lptfoybu"><a href="#delete-a-file" style="--depth: 1;" class="astro-lptfoybu"><span style="--depth: 1;" class="astro-lptfoybu">Delete a file</span></a></li><li style="--depth: 1;" class="astro-lptfoybu"><a href="#reading-media-from-a-frontend" style="--depth: 1;" class="astro-lptfoybu"><span style="--depth: 1;" class="astro-lptfoybu">Reading media from a frontend</span></a></li></ul></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#next" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Next</span></a></li></ul></nav></starlight-toc><script type="module" src="/docs/_astro/TableOfContents.astro_astro_type_script_index_0_lang.Csloo1VZ.js"></script></div></div></div></aside><div class="main-pane astro-perbdhd2"><main data-pagefind-body class="astro-ewhz5xxr" lang="en" dir="ltr"><div class="content-panel astro-ahkzlqop"><div class="sl-container astro-ahkzlqop"><h1 id="_top" class="astro-oef2efwn">Media</h1></div></div><div class="content-panel astro-ahkzlqop"><div class="sl-container astro-ahkzlqop"><div class="sl-markdown-content"><p>Velora splits every uploaded file in two: the <strong>bytes</strong> go to a storage
|
|
74
|
+
driver (local disk by default, or an S3-compatible object store), and a
|
|
75
|
+
<strong>metadata row</strong> — filename, MIME type, size, uploader, a public/private
|
|
76
|
+
flag, and a key pointing at the object — goes into Velora’s own database.
|
|
77
|
+
The CMS itself serves the bytes, at one stable, permanent URL per file:
|
|
78
|
+
<code dir="auto">GET /media/:id/:filename</code>. A <strong>public</strong> file’s URL never changes and never
|
|
79
|
+
expires — safe to embed in content. A <strong>private</strong> file’s URL 404s for
|
|
80
|
+
anyone without a session, an API key, or a short-lived temporary link.
|
|
78
81
|
Everything on this page follows from that split.</p>
|
|
79
82
|
<div class="sl-heading-wrapper level-h2"><h2 id="the-media-library">The media library</h2><a class="sl-anchor-link" href="#the-media-library"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42"></path></svg></span><span class="sr-only" data-pagefind-ignore="">Section titled “The media library”</span></a></div>
|
|
80
83
|
<p>The <strong>Media</strong> section is a flat, searchable library of every file this
|
|
@@ -154,15 +157,37 @@ that confirmation really is final.</p>
|
|
|
154
157
|
|
|
155
158
|
|
|
156
159
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
<table><thead><tr><th>Name</th><th>Type</th><th>Meaning</th><th>Example</th></tr></thead><tbody><tr><td><code dir="auto">id</code></td><td>string (uuid)</td><td>The file’s identity. This is the value an Image field stores, and the <code dir="auto">:id</code> in every media route.</td><td><code dir="auto">9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218</code></td></tr><tr><td><code dir="auto">filename</code></td><td>string, non-empty</td><td>The original filename, kept verbatim — spaces and non-ASCII characters included. Only the storage key is sanitised, never this.</td><td><code dir="auto">route test file.txt</code></td></tr><tr><td><code dir="auto">mimeType</code></td><td>string, non-empty</td><td>The content type the browser sent with the upload part. Stored as given and replayed as the object’s <code dir="auto">Content-Type</code>.</td><td><code dir="auto">text/plain</code></td></tr><tr><td><code dir="auto">size</code></td><td>integer ≥ 0</td><td>Byte length of the uploaded buffer, measured server-side — not a client-declared value.</td><td><code dir="auto">42</code></td></tr><tr><td><code dir="auto">url</code></td><td>string (path)</td><td>The <strong>stable app path</strong>, <code dir="auto">/media/<id>/<filename></code> — computed from the row, never stored as an absolute host/port/endpoint that could go stale. Identical to <code dir="auto">downloadUrl</code> for a public item. Admin responses only.</td><td><code dir="auto">/media/9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218/route_test_file.txt</code></td></tr><tr><td><code dir="auto">visibility</code></td><td><code dir="auto">"public"</code> | <code dir="auto">"private"</code></td><td>Whether the stable URL above serves anonymously. Default <code dir="auto">public</code>. See <a href="#visibility-public-and-private">Visibility</a> below.</td><td><code dir="auto">public</code></td></tr><tr><td><code dir="auto">uploadedBy</code></td><td>string (uuid)</td><td>The user who uploaded it, taken from the session token, never from the request. Admin responses only.</td><td><code dir="auto">0b6d4e21-8c3a-4f57-9d02-6a1e5b7c8d90</code></td></tr><tr><td><code dir="auto">uploadedAt</code></td><td>ISO datetime string</td><td>When the row was written. The library sorts on this, newest first, with <code dir="auto">id</code> breaking ties.</td><td><code dir="auto">2026-08-31T09:12:04.006Z</code></td></tr><tr><td><code dir="auto">downloadUrl</code></td><td>string (path)</td><td>The same stable path as <code dir="auto">url</code> for a <strong>public</strong> item — permanent, safe to persist or embed. For a <strong>private</strong> item, that path plus a freshly minted <code dir="auto">?token=</code> good for 15 minutes: a new one on every response, meant for immediate use (rendering a thumbnail now), not for saving.</td><td><code dir="auto">/media/9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218/route_test_file.txt</code></td></tr></tbody></table>
|
|
167
|
+
<p><code dir="auto">downloadUrl</code> is the only property here that is computed rather than read
|
|
168
|
+
straight from the row: it’s what adds the temporary token for a private
|
|
169
|
+
item, so no media has to be world-readable for the admin to show a
|
|
170
|
+
thumbnail.</p>
|
|
161
171
|
<div class="sl-heading-wrapper level-h2"><h2 id="where-the-bytes-live">Where the bytes live</h2><a class="sl-anchor-link" href="#where-the-bytes-live"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42"></path></svg></span><span class="sr-only" data-pagefind-ignore="">Section titled “Where the bytes live”</span></a></div>
|
|
162
|
-
<p>
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
172
|
+
<p>Velora talks to storage through one interface with two implementations, so
|
|
173
|
+
neither the CMS nor a plugin ever has to know which is active:</p>
|
|
174
|
+
<ul>
|
|
175
|
+
<li><strong>Local disk</strong> — the scaffold default. Uploads land under <code dir="auto">MEDIA_DIR</code>
|
|
176
|
+
(default <code dir="auto">./media</code>, resolved against the working directory), right next
|
|
177
|
+
to <code dir="auto">velora.db</code>. There is no object store to run for a simple site: back
|
|
178
|
+
up the project folder and you’ve backed up the uploads too.</li>
|
|
179
|
+
<li><strong>S3-compatible object storage</strong> — for a deployment that wants to scale
|
|
180
|
+
storage independently, or already runs S3/R2/MinIO. One private bucket,
|
|
181
|
+
same keys as the local driver; the server is still the only thing that
|
|
182
|
+
ever serves the bytes.</li>
|
|
183
|
+
</ul>
|
|
184
|
+
<p>Which one runs is decided at boot, in order: <code dir="auto">MEDIA_STORAGE=local</code> or
|
|
185
|
+
<code dir="auto">MEDIA_STORAGE=s3</code> wins outright if set; otherwise, any <code dir="auto">S3_*</code> variable
|
|
186
|
+
being set (non-empty) selects <code dir="auto">s3</code>; with nothing set at all, a fresh
|
|
187
|
+
scaffold gets local disk automatically. See <code dir="auto">MEDIA_STORAGE</code> and <code dir="auto">MEDIA_DIR</code>
|
|
188
|
+
in <a href="/docs/v1/using-velora/installation/#media-storage">Installation</a> for the
|
|
189
|
+
full variable reference, alongside the five <code dir="auto">S3_*</code> variables an S3
|
|
190
|
+
deployment needs:</p>
|
|
166
191
|
|
|
167
192
|
|
|
168
193
|
|
|
@@ -205,14 +230,62 @@ a real provider.</p>
|
|
|
205
230
|
|
|
206
231
|
|
|
207
232
|
<table><thead><tr><th>Name</th><th>Type</th><th>Meaning</th><th>Example</th></tr></thead><tbody><tr><td><code dir="auto">S3_ENDPOINT</code></td><td>string (URL)</td><td>Object storage endpoint. Requests are path-style, so a <code dir="auto">localhost</code> endpoint works. Default <code dir="auto">http://localhost:9000</code>.</td><td><code dir="auto">https://s3.eu-central-1.amazonaws.com</code></td></tr><tr><td><code dir="auto">S3_REGION</code></td><td>string</td><td>Region passed to the S3 client. Default <code dir="auto">us-east-1</code>.</td><td><code dir="auto">eu-central-1</code></td></tr><tr><td><code dir="auto">S3_ACCESS_KEY_ID</code></td><td>string</td><td>Access key id. Default <code dir="auto">velora</code>.</td><td><code dir="auto">AKIA…</code></td></tr><tr><td><code dir="auto">S3_SECRET_ACCESS_KEY</code></td><td>string</td><td>Secret access key. Default <code dir="auto">velora-secret</code>.</td><td><code dir="auto">wJalr…</code></td></tr><tr><td><code dir="auto">S3_BUCKET</code></td><td>string</td><td>Bucket the objects land in. Created on startup if missing. Default <code dir="auto">velora-media</code>.</td><td><code dir="auto">velora-media</code></td></tr></tbody></table>
|
|
208
|
-
<p>
|
|
209
|
-
replaces every character outside <code dir="auto">A–Z a–z 0–9 . _ -</code> with <code dir="auto">_</code>.
|
|
210
|
-
with the id means two uploads of <code dir="auto">logo.png</code> never collide, and
|
|
211
|
-
filename still survives untouched in the metadata row.</p>
|
|
233
|
+
<p>In both drivers the object key is <code dir="auto"><media id>/<sanitised filename></code>, where
|
|
234
|
+
sanitising replaces every character outside <code dir="auto">A–Z a–z 0–9 . _ -</code> with <code dir="auto">_</code>.
|
|
235
|
+
Prefixing with the id means two uploads of <code dir="auto">logo.png</code> never collide, and
|
|
236
|
+
the original filename still survives untouched in the metadata row.</p>
|
|
212
237
|
<p>The two writes are ordered so metadata stays the source of truth: the
|
|
213
238
|
object is stored first, then the row; if the row insert fails, the
|
|
214
239
|
just-written object is deleted again. There is no path that leaves an
|
|
215
240
|
object no row points at.</p>
|
|
241
|
+
<div class="sl-heading-wrapper level-h2"><h2 id="visibility-public-and-private">Visibility: public and private</h2><a class="sl-anchor-link" href="#visibility-public-and-private"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42"></path></svg></span><span class="sr-only" data-pagefind-ignore="">Section titled “Visibility: public and private”</span></a></div>
|
|
242
|
+
<p>Every media item carries a <code dir="auto">visibility</code> flag, <code dir="auto">public</code> or <code dir="auto">private</code> —
|
|
243
|
+
defaulting to <code dir="auto">public</code>, the normal case for images embedded in content.
|
|
244
|
+
It’s a checkbox on each item in the media library (also shown as a
|
|
245
|
+
<strong>Private</strong> badge on the thumbnail), and flipping it only ever updates that
|
|
246
|
+
one row: no file move, no key change, and — deliberately — no URL change,
|
|
247
|
+
in either driver.</p>
|
|
248
|
+
<ul>
|
|
249
|
+
<li><strong>Public</strong> items serve from <code dir="auto">GET /media/:id/:filename</code> with no
|
|
250
|
+
credentials, and the response carries a permanent, cacheable header
|
|
251
|
+
(<code dir="auto">Cache-Control: public, max-age=31536000, immutable</code>) — safe to embed
|
|
252
|
+
directly in published content.</li>
|
|
253
|
+
<li><strong>Private</strong> items answer that same URL only for a signed-in admin
|
|
254
|
+
session, a read-scoped API key, or a valid <code dir="auto">?token=</code> (see <a href="#temporary-links-for-private-files">Temporary
|
|
255
|
+
links</a> below). Everyone else gets a
|
|
256
|
+
<code dir="auto">404</code> — the same body an unknown id returns, so the response never
|
|
257
|
+
reveals whether a private id exists at all.</li>
|
|
258
|
+
</ul>
|
|
259
|
+
<p>One caveat worth knowing before you rely on flipping a file private: a
|
|
260
|
+
browser or CDN that already cached a public copy under that permanent
|
|
261
|
+
header can keep serving it — to whoever holds that copy — for up to a
|
|
262
|
+
year after you check the box. The visibility check only governs <em>new</em>
|
|
263
|
+
requests; there’s no cache-purge step on flip, since the URL is
|
|
264
|
+
deliberately stable across the change. The admin’s help text under the
|
|
265
|
+
checkbox says the same thing.</p>
|
|
266
|
+
<p>Certain file types always download instead of rendering inline — <code dir="auto">text/html</code>,
|
|
267
|
+
<code dir="auto">image/svg+xml</code>, and other markup/script-capable types — even for a public
|
|
268
|
+
item, so a hostile upload can never execute script on the CMS’s own origin.
|
|
269
|
+
Ordinary images, video, audio, PDF, and plain text still render inline as
|
|
270
|
+
you’d expect.</p>
|
|
271
|
+
<div class="sl-heading-wrapper level-h2"><h2 id="temporary-links-for-private-files">Temporary links for private files</h2><a class="sl-anchor-link" href="#temporary-links-for-private-files"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42"></path></svg></span><span class="sr-only" data-pagefind-ignore="">Section titled “Temporary links for private files”</span></a></div>
|
|
272
|
+
<p>A private item needs a way to hand out a link without opening a session —
|
|
273
|
+
a review link, an email attachment. The admin’s <strong>Copy temporary link</strong>
|
|
274
|
+
button (shown next to a private item only) mints one:</p>
|
|
275
|
+
<div class="expressive-code"><link rel="stylesheet" href="/docs/_astro/ec.w36nc.css"><script type="module" src="/docs/_astro/ec.0vx5m.js"></script><figure class="frame is-terminal not-content"><figcaption class="header"><span class="title"></span><span class="sr-only">Terminal window</span></figcaption><pre data-language="sh"><code><div class="ec-line"><div class="code"><span style="--0:#82AAFF;--1:#3B61B0">curl</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#82AAFF;--1:#3B61B0">-s</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#82AAFF;--1:#3B61B0">-X</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#ECC48D;--1:#3B61B0">POST</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#ECC48D;--1:#3B61B0">http://localhost:3000/api/media/9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218/link</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#F78C6C;--1:#AA0982">\</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#82AAFF;--1:#3B61B0">-H</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#ECC48D;--1:#984E4D">Authorization: Bearer </span><span style="--0:#C5E478;--1:#3B61B0">$TOKEN</span><span style="--0:#D9F5DD;--1:#111111">"</span></div></div></code></pre><div class="copy"><div aria-live="polite"></div><button title="Copy to clipboard" data-copied="Copied!" data-code="curl -s -X POST http://localhost:3000/api/media/9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218/link \ -H "Authorization: Bearer $TOKEN""><div></div></button></div></figure></div>
|
|
276
|
+
<p>The response is the stable URL with a signed <code dir="auto">?token=</code> appended, and when
|
|
277
|
+
it stops working:</p>
|
|
278
|
+
<div class="expressive-code"><figure class="frame not-content"><figcaption class="header"></figcaption><pre data-language="json"><code><div class="ec-line"><div class="code"><span style="--0:#D6DEEB;--1:#403F53">{</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"url"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">/media/9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218/cover.png?token=eyJpZCI6…</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"expiresAt"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">2026-09-02T09:27:04.006Z</span><span style="--0:#D9F5DD;--1:#111111">"</span></div></div><div class="ec-line"><div class="code"><span style="--0:#D6DEEB;--1:#403F53">}</span></div></div></code></pre><div class="copy"><div aria-live="polite"></div><button title="Copy to clipboard" data-copied="Copied!" data-code="{ "url": "/media/9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218/cover.png?token=eyJpZCI6…", "expiresAt": "2026-09-02T09:27:04.006Z"}"><div></div></button></div></figure></div>
|
|
279
|
+
<p>The token defaults to 15 minutes and is scoped to that one media id — it
|
|
280
|
+
does nothing against any other item’s URL, and it stops working outright
|
|
281
|
+
past <code dir="auto">expiresAt</code> (a <code dir="auto">404</code>, same as every other rejection this route makes).
|
|
282
|
+
Minting a new one doesn’t invalidate an older still-live token; there’s no
|
|
283
|
+
revoke, so treat a copied link as good until it naturally expires.</p>
|
|
284
|
+
<div class="sl-heading-wrapper level-h2"><h2 id="renditions-reserved">Renditions (reserved)</h2><a class="sl-anchor-link" href="#renditions-reserved"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42"></path></svg></span><span class="sr-only" data-pagefind-ignore="">Section titled “Renditions (reserved)”</span></a></div>
|
|
285
|
+
<p><code dir="auto">?preset=<name></code> and <code dir="auto">?w=<px></code> are reserved on <code dir="auto">GET /media/:id/:filename</code>
|
|
286
|
+
for a future crop/scale feature — the route rejects both with a <code dir="auto">400</code>
|
|
287
|
+
today rather than silently ignoring them, so that a future implementation
|
|
288
|
+
isn’t a silent behavior change for anyone already using them.</p>
|
|
216
289
|
<div class="sl-heading-wrapper level-h2"><h2 id="limits-and-what-is-not-checked">Limits, and what is not checked</h2><a class="sl-anchor-link" href="#limits-and-what-is-not-checked"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42"></path></svg></span><span class="sr-only" data-pagefind-ignore="">Section titled “Limits, and what is not checked”</span></a></div>
|
|
217
290
|
|
|
218
291
|
|
|
@@ -254,11 +327,13 @@ your proxy or object store.</p>
|
|
|
254
327
|
<p>The <strong>Image</strong> data type (editor id <code dir="auto">com.velora.image</code>) does not embed a
|
|
255
328
|
copy of the file. Its stored value is the media id — one string — so a
|
|
256
329
|
content version’s <code dir="auto">data</code> for an Image field looks like this:</p>
|
|
257
|
-
<div class="expressive-code"><
|
|
330
|
+
<div class="expressive-code"><figure class="frame not-content"><figcaption class="header"></figcaption><pre data-language="json"><code><div class="ec-line"><div class="code"><span style="--0:#D6DEEB;--1:#403F53">{ </span><span style="--0:#7FDBCA;--1:#096E72">"coverImage"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53"> }</span></div></div></code></pre><div class="copy"><div aria-live="polite"></div><button title="Copy to clipboard" data-copied="Copied!" data-code="{ "coverImage": "9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218" }"><div></div></button></div></figure></div>
|
|
258
331
|
<p>That is the whole value: no filename, no URL, no size. The field fetches
|
|
259
|
-
<code dir="auto">GET /api/media/:id</code> whenever the id changes and renders the
|
|
260
|
-
|
|
261
|
-
|
|
332
|
+
<code dir="auto">GET /api/media/:id</code> whenever the id changes and renders the <code dir="auto">downloadUrl</code>
|
|
333
|
+
it gets back — the permanent stable path for a public item, or a
|
|
334
|
+
freshly tokened one for a private item — which is why nothing in your
|
|
335
|
+
content has to be rewritten when a file’s visibility flips or storage
|
|
336
|
+
moves.</p>
|
|
262
337
|
|
|
263
338
|
|
|
264
339
|
|
|
@@ -288,8 +363,8 @@ the thumbnail reports that the media could not be loaded. Treat “delete” as
|
|
|
288
363
|
<div class="sl-heading-wrapper level-h2"><h2 id="the-api">The API</h2><a class="sl-anchor-link" href="#the-api"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42"></path></svg></span><span class="sr-only" data-pagefind-ignore="">Section titled “The API”</span></a></div>
|
|
289
364
|
<p>The JSON in this section is derived from the Zod schemas in
|
|
290
365
|
<code dir="auto">packages/api-schemas</code> and the shapes the route integration tests assert —
|
|
291
|
-
ids
|
|
292
|
-
|
|
366
|
+
ids and timestamps are illustrative. The two live transcripts against the
|
|
367
|
+
public demo are labelled where they appear.</p>
|
|
293
368
|
<p>Every <code dir="auto">/api/media</code> route needs a signed-in session: send
|
|
294
369
|
<code dir="auto">Authorization: Bearer <accessToken></code> from <code dir="auto">POST /api/auth/login</code>. Without
|
|
295
370
|
it each one answers <code dir="auto">401</code>.</p>
|
|
@@ -298,9 +373,9 @@ it each one answers <code dir="auto">401</code>.</p>
|
|
|
298
373
|
name is not significant — the route takes the first file part it finds —
|
|
299
374
|
but it must carry a filename:</p>
|
|
300
375
|
<div class="expressive-code"><figure class="frame is-terminal not-content"><figcaption class="header"><span class="title"></span><span class="sr-only">Terminal window</span></figcaption><pre data-language="sh"><code><div class="ec-line"><div class="code"><span style="--0:#82AAFF;--1:#3B61B0">curl</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#82AAFF;--1:#3B61B0">-s</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#82AAFF;--1:#3B61B0">-X</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#ECC48D;--1:#3B61B0">POST</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#ECC48D;--1:#3B61B0">http://localhost:3000/api/media</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#F78C6C;--1:#AA0982">\</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#82AAFF;--1:#3B61B0">-H</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#ECC48D;--1:#984E4D">Authorization: Bearer </span><span style="--0:#C5E478;--1:#3B61B0">$TOKEN</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#F78C6C;--1:#AA0982">\</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#82AAFF;--1:#3B61B0">-F</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#ECC48D;--1:#984E4D">file=@./cover.png</span><span style="--0:#D9F5DD;--1:#111111">"</span></div></div></code></pre><div class="copy"><div aria-live="polite"></div><button title="Copy to clipboard" data-copied="Copied!" data-code="curl -s -X POST http://localhost:3000/api/media \ -H "Authorization: Bearer $TOKEN" \ -F "file=@./cover.png""><div></div></button></div></figure></div>
|
|
301
|
-
<p>A success is <code dir="auto">201</code> with the full record,
|
|
302
|
-
<code dir="auto">downloadUrl</code
|
|
303
|
-
<div class="expressive-code"><figure class="frame not-content"><figcaption class="header"></figcaption><pre data-language="json"><code><div class="ec-line"><div class="code"><span style="--0:#D6DEEB;--1:#403F53">{</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"id"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"filename"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">cover.png</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"mimeType"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">image/png</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"size"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#F78C6C;--1:#AA0982">184320</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"url"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686"
|
|
376
|
+
<p>A success is <code dir="auto">201</code> with the full record, <code dir="auto">visibility</code> defaulted to
|
|
377
|
+
<code dir="auto">public</code> and <code dir="auto">downloadUrl</code> equal to the permanent stable path:</p>
|
|
378
|
+
<div class="expressive-code"><figure class="frame not-content"><figcaption class="header"></figcaption><pre data-language="json"><code><div class="ec-line"><div class="code"><span style="--0:#D6DEEB;--1:#403F53">{</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"id"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"filename"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">cover.png</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"mimeType"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">image/png</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"size"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#F78C6C;--1:#AA0982">184320</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"url"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">/media/9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218/cover.png</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"visibility"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">public</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"uploadedBy"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">0b6d4e21-8c3a-4f57-9d02-6a1e5b7c8d90</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"uploadedAt"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">2026-08-31T09:12:04.006Z</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"downloadUrl"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">/media/9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218/cover.png</span><span style="--0:#D9F5DD;--1:#111111">"</span></div></div><div class="ec-line"><div class="code"><span style="--0:#D6DEEB;--1:#403F53">}</span></div></div></code></pre><div class="copy"><div aria-live="polite"></div><button title="Copy to clipboard" data-copied="Copied!" data-code="{ "id": "9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218", "filename": "cover.png", "mimeType": "image/png", "size": 184320, "url": "/media/9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218/cover.png", "visibility": "public", "uploadedBy": "0b6d4e21-8c3a-4f57-9d02-6a1e5b7c8d90", "uploadedAt": "2026-08-31T09:12:04.006Z", "downloadUrl": "/media/9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218/cover.png"}"><div></div></button></div></figure></div>
|
|
304
379
|
<p>A request with no file part is a <code dir="auto">400</code> with
|
|
305
380
|
<code dir="auto">{"message":"A multipart file field is required"}</code> — that, and the 50 MiB
|
|
306
381
|
ceiling, are the only two ways an upload is refused.</p>
|
|
@@ -309,8 +384,8 @@ ceiling, are the only two ways an upload is refused.</p>
|
|
|
309
384
|
case-insensitive filename filter:</p>
|
|
310
385
|
<div class="expressive-code"><figure class="frame is-terminal not-content"><figcaption class="header"><span class="title"></span><span class="sr-only">Terminal window</span></figcaption><pre data-language="sh"><code><div class="ec-line"><div class="code"><span style="--0:#82AAFF;--1:#3B61B0">curl</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#82AAFF;--1:#3B61B0">-s</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#ECC48D;--1:#984E4D">http://localhost:3000/api/media?limit=2&search=cover</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#F78C6C;--1:#AA0982">\</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#82AAFF;--1:#3B61B0">-H</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#ECC48D;--1:#984E4D">Authorization: Bearer </span><span style="--0:#C5E478;--1:#3B61B0">$TOKEN</span><span style="--0:#D9F5DD;--1:#111111">"</span></div></div></code></pre><div class="copy"><div aria-live="polite"></div><button title="Copy to clipboard" data-copied="Copied!" data-code="curl -s "http://localhost:3000/api/media?limit=2&search=cover" \ -H "Authorization: Bearer $TOKEN""><div></div></button></div></figure></div>
|
|
311
386
|
<p>It answers <code dir="auto">200</code> with a page of full records — each one carrying its own
|
|
312
|
-
freshly
|
|
313
|
-
<div class="expressive-code"><figure class="frame not-content"><figcaption class="header"></figcaption><pre data-language="json"><code><div class="ec-line"><div class="code"><span style="--0:#D6DEEB;--1:#403F53">{</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"items"</span><span style="--0:#D6DEEB;--1:#403F53">: [</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#D6DEEB;--1:#403F53"> </span></span><span style="--0:#D6DEEB;--1:#403F53">{</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"id"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"filename"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">cover.png</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"mimeType"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">image/png</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"size"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#F78C6C;--1:#AA0982">184320</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"url"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686"
|
|
387
|
+
freshly computed <code dir="auto">downloadUrl</code> — and the cursor for the next page:</p>
|
|
388
|
+
<div class="expressive-code"><figure class="frame not-content"><figcaption class="header"></figcaption><pre data-language="json"><code><div class="ec-line"><div class="code"><span style="--0:#D6DEEB;--1:#403F53">{</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"items"</span><span style="--0:#D6DEEB;--1:#403F53">: [</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#D6DEEB;--1:#403F53"> </span></span><span style="--0:#D6DEEB;--1:#403F53">{</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"id"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"filename"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">cover.png</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"mimeType"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">image/png</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"size"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#F78C6C;--1:#AA0982">184320</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"url"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">/media/9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218/cover.png</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"visibility"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">public</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"uploadedBy"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">0b6d4e21-8c3a-4f57-9d02-6a1e5b7c8d90</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"uploadedAt"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">2026-08-31T09:12:04.006Z</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"downloadUrl"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">/media/9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218/cover.png</span><span style="--0:#D9F5DD;--1:#111111">"</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#D6DEEB;--1:#403F53"> </span></span><span style="--0:#D6DEEB;--1:#403F53">}</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#D6DEEB;--1:#403F53"> </span></span><span style="--0:#D6DEEB;--1:#403F53">],</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"nextCursor"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218</span><span style="--0:#D9F5DD;--1:#111111">"</span></div></div><div class="ec-line"><div class="code"><span style="--0:#D6DEEB;--1:#403F53">}</span></div></div></code></pre><div class="copy"><div aria-live="polite"></div><button title="Copy to clipboard" data-copied="Copied!" data-code="{ "items": [ { "id": "9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218", "filename": "cover.png", "mimeType": "image/png", "size": 184320, "url": "/media/9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218/cover.png", "visibility": "public", "uploadedBy": "0b6d4e21-8c3a-4f57-9d02-6a1e5b7c8d90", "uploadedAt": "2026-08-31T09:12:04.006Z", "downloadUrl": "/media/9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218/cover.png" } ], "nextCursor": "9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218"}"><div></div></button></div></figure></div>
|
|
314
389
|
|
|
315
390
|
|
|
316
391
|
|
|
@@ -390,8 +465,11 @@ transcript rather than a derived one:</p>
|
|
|
390
465
|
has nothing to fetch, so this one is derived from <code dir="auto">PublicMediaFileSchema</code>
|
|
391
466
|
rather than a live transcript:</p>
|
|
392
467
|
<div class="expressive-code"><figure class="frame is-terminal not-content"><figcaption class="header"><span class="title"></span><span class="sr-only">Terminal window</span></figcaption><pre data-language="sh"><code><div class="ec-line"><div class="code"><span style="--0:#82AAFF;--1:#3B61B0">curl</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#82AAFF;--1:#3B61B0">-s</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#82AAFF;--1:#3B61B0">-H</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#ECC48D;--1:#984E4D">Authorization: Bearer vk_4ac0a6b78b953a13b8646e6379fcbb6bee1a269af253a14eebce668572019328</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#F78C6C;--1:#AA0982">\</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#ECC48D;--1:#984E4D">https://demo.velora-cms.com/api/v1/media/9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218</span><span style="--0:#D9F5DD;--1:#111111">"</span></div></div></code></pre><div class="copy"><div aria-live="polite"></div><button title="Copy to clipboard" data-copied="Copied!" data-code="curl -s -H "Authorization: Bearer vk_4ac0a6b78b953a13b8646e6379fcbb6bee1a269af253a14eebce668572019328" \ "https://demo.velora-cms.com/api/v1/media/9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218""><div></div></button></div></figure></div>
|
|
393
|
-
<p>A hit looks like this
|
|
394
|
-
|
|
468
|
+
<p>A hit looks like this — <code dir="auto">downloadUrl</code> here is <strong>absolute</strong>, unlike the
|
|
469
|
+
admin surface’s path-relative <code dir="auto">url</code>, because a headless frontend fetches it
|
|
470
|
+
from its own origin (see <a href="/docs/v1/using-velora/headless-api/">Headless API</a> for
|
|
471
|
+
how the base is chosen):</p>
|
|
472
|
+
<div class="expressive-code"><figure class="frame not-content"><figcaption class="header"></figcaption><pre data-language="json"><code><div class="ec-line"><div class="code"><span style="--0:#D6DEEB;--1:#403F53">{</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"id"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"filename"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">cover.png</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"mimeType"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">image/png</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"size"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#F78C6C;--1:#AA0982">184320</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"downloadUrl"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">https://demo.velora-cms.com/media/9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218/cover.png</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"visibility"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">public</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"uploadedAt"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">2026-08-31T09:12:04.006Z</span><span style="--0:#D9F5DD;--1:#111111">"</span></div></div><div class="ec-line"><div class="code"><span style="--0:#D6DEEB;--1:#403F53">}</span></div></div></code></pre><div class="copy"><div aria-live="polite"></div><button title="Copy to clipboard" data-copied="Copied!" data-code="{ "id": "9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218", "filename": "cover.png", "mimeType": "image/png", "size": 184320, "downloadUrl": "https://demo.velora-cms.com/media/9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218/cover.png", "visibility": "public", "uploadedAt": "2026-08-31T09:12:04.006Z"}"><div></div></button></div></figure></div>
|
|
395
473
|
<p>Asking for an id that does not exist returns <code dir="auto">404</code> and the same message
|
|
396
474
|
shape as the admin route — also a live response:</p>
|
|
397
475
|
<div class="expressive-code"><figure class="frame not-content"><figcaption class="header"></figcaption><pre data-language="json"><code><div class="ec-line"><div class="code"><span style="--0:#D6DEEB;--1:#403F53">{</span><span style="--0:#7FDBCA;--1:#096E72">"message"</span><span style="--0:#D6DEEB;--1:#403F53">:</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">No media file found for id 00000000-0000-4000-8000-000000000000</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">}</span></div></div></code></pre><div class="copy"><div aria-live="polite"></div><button title="Copy to clipboard" data-copied="Copied!" data-code="{"message":"No media file found for id 00000000-0000-4000-8000-000000000000"}"><div></div></button></div></figure></div>
|
|
@@ -447,10 +525,16 @@ than the admin one:</p>
|
|
|
447
525
|
|
|
448
526
|
|
|
449
527
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
<table><thead><tr><th>Name</th><th>Type</th><th>Meaning</th><th>Example</th></tr></thead><tbody><tr><td><code dir="auto">id</code></td><td>string (uuid)</td><td>Same id as the admin surface — the value your content’s Image fields contain.</td><td><code dir="auto">9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218</code></td></tr><tr><td><code dir="auto">filename</code></td><td>string, non-empty</td><td>Original filename. Useful as <code dir="auto">alt</code> text fallback.</td><td><code dir="auto">cover.png</code></td></tr><tr><td><code dir="auto">mimeType</code></td><td>string, non-empty</td><td>Content type, for picking a renderer.</td><td><code dir="auto">image/png</code></td></tr><tr><td><code dir="auto">size</code></td><td>integer ≥ 0</td><td>Byte length.</td><td><code dir="auto">184320</code></td></tr><tr><td><code dir="auto">downloadUrl</code></td><td>string (absolute URL)</td><td>The stable path made absolute (<code dir="auto">VELORA_PUBLIC_URL</code>, or derived from the request). Permanent for a public item. For a private one it’s minted with a fresh short-lived <code dir="auto">?token=</code> right here — your API key is enough to get a working link back — but that link stops working once the token expires, and <em>any other</em> fetch of the bare path 404s exactly like an unknown id (no existence oracle). Fetch the record at render time rather than persisting this.</td><td><code dir="auto">https://cms.example.com/media/9f1d3b7c…/cover.png</code></td></tr><tr><td><code dir="auto">visibility</code></td><td><code dir="auto">"public"</code> | <code dir="auto">"private"</code></td><td>Whether the URL above needed a token.</td><td><code dir="auto">public</code></td></tr><tr><td><code dir="auto">uploadedAt</code></td><td>ISO datetime string</td><td>When it was uploaded.</td><td><code dir="auto">2026-08-31T09:12:04.006Z</code></td></tr></tbody></table>
|
|
535
|
+
<p><code dir="auto">uploadedBy</code> and the path-relative <code dir="auto">url</code> are omitted on purpose: one is an
|
|
536
|
+
internal user id, the other is meaningless to a client on a different
|
|
537
|
+
origin.</p>
|
|
454
538
|
<p>One thing to know before you publish a key: unlike content, <strong>media has no
|
|
455
539
|
published state</strong>, so <code dir="auto">/api/v1/media</code> lists every uploaded file, including
|
|
456
540
|
ones no content references yet. <code dir="auto">?limit=</code> and <code dir="auto">?cursor=</code> behave exactly as
|
package/dist/graphql/schema.d.ts
CHANGED
package/dist/graphql/schema.js
CHANGED
|
@@ -86,6 +86,9 @@ const PublicMediaFileRef = builder.objectRef("PublicMediaFile").implement({
|
|
|
86
86
|
mimeType: t.exposeString("mimeType"),
|
|
87
87
|
size: t.exposeInt("size"),
|
|
88
88
|
downloadUrl: t.exposeString("downloadUrl"),
|
|
89
|
+
// S186c §2 — public/private, same value REST's PublicMediaFileSchema
|
|
90
|
+
// carries; a headless consumer needs to know before downloadUrl 401s.
|
|
91
|
+
visibility: t.exposeString("visibility"),
|
|
89
92
|
uploadedAt: t.exposeString("uploadedAt"),
|
|
90
93
|
}),
|
|
91
94
|
});
|
|
@@ -307,7 +310,7 @@ builder.queryType({
|
|
|
307
310
|
args: { id: t.arg.id({ required: true }) },
|
|
308
311
|
resolve: async (_root, args, ctx) => {
|
|
309
312
|
const media = await ctx.mediaService.getById(String(args.id));
|
|
310
|
-
return media ? toPublicMediaFile(ctx.mediaService, media) : null;
|
|
313
|
+
return media ? toPublicMediaFile(ctx.mediaService, media, ctx.mediaBaseUrl) : null;
|
|
311
314
|
},
|
|
312
315
|
}),
|
|
313
316
|
// GET /api/v1/media analog — never null, same as contentRoots.
|
|
@@ -318,7 +321,7 @@ builder.queryType({
|
|
|
318
321
|
resolve: async (_root, args, ctx) => {
|
|
319
322
|
const { cursor, limit, search } = parseArgs(PublicMediaListQuerySchema, { cursor: args.cursor ?? undefined, limit: args.limit ?? undefined, search: args.search ?? undefined }, "media arguments");
|
|
320
323
|
const files = await ctx.mediaService.list({ cursor, limit, search });
|
|
321
|
-
const items = await Promise.all(files.map((file) => toPublicMediaFile(ctx.mediaService, file)));
|
|
324
|
+
const items = await Promise.all(files.map((file) => toPublicMediaFile(ctx.mediaService, file, ctx.mediaBaseUrl)));
|
|
322
325
|
const nextCursor = files.length === limit ? (files[files.length - 1]?.id ?? null) : null;
|
|
323
326
|
return { items, nextCursor };
|
|
324
327
|
},
|
package/dist/lib.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export declare function createApp(): Promise<import("fastify").FastifyInstance<import("fastify").RawServerDefault, import("http").IncomingMessage, import("http").ServerResponse<import("http").IncomingMessage>, import("fastify").FastifyBaseLogger, import("fastify").FastifyTypeProviderDefault>>;
|
|
2
2
|
export declare const buildApp: typeof createApp;
|
|
3
|
+
export { syncAdminBundle } from "./admin/sync-admin-bundle.js";
|
package/dist/lib.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// The embed surface (S185). createApp() mirrors the preparation the
|
|
2
2
|
// `velora-server` bin (index.ts) performs before building the app — env
|
|
3
3
|
// file load, DATABASE_URL normalization, Prisma-client self-heal, core
|
|
4
|
-
// migrations — then returns the built (not-listening)
|
|
5
|
-
// the caller to mount or listen on themselves.
|
|
4
|
+
// migrations, boot-mode resolution — then returns the built (not-listening)
|
|
5
|
+
// Fastify instance for the caller to mount or listen on themselves.
|
|
6
6
|
//
|
|
7
7
|
// Everything below is imported DYNAMICALLY and only inside the call:
|
|
8
8
|
// db/prisma.ts holds a top-level-await Prisma client that must not be
|
|
@@ -10,13 +10,27 @@
|
|
|
10
10
|
// point), and a consumer must be able to set process.env AFTER importing
|
|
11
11
|
// this module and BEFORE calling createApp().
|
|
12
12
|
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
13
|
+
// Rehearsal finding (S186b follow-up): createApp() used to unconditionally
|
|
14
|
+
// dynamic-import ./app.js, whose import graph reaches db/prisma.ts's
|
|
15
|
+
// top-level-await Prisma singleton — so on a fresh scaffold (no
|
|
16
|
+
// DATABASE_URL, no generated client) it threw "@prisma/client did not
|
|
17
|
+
// initialize" before the setup wizard could ever be served. The bin
|
|
18
|
+
// (index.ts) never had this problem because it resolves the boot mode
|
|
19
|
+
// FIRST and only imports app.js for "full". createApp() now does the same:
|
|
20
|
+
// resolveBootMode decides whether this process is unconfigured
|
|
21
|
+
// (setup-full), configured-but-adminless (setup-admin-only), or ready
|
|
22
|
+
// (full), and only the "full" branch touches app.js/the Prisma singleton
|
|
23
|
+
// at all — the two setup-* branches build the same setup-only app index.ts
|
|
24
|
+
// does, via buildSetupOnlyApp(), and never import app.js.
|
|
25
|
+
//
|
|
26
|
+
// Unlike index.ts, this does NOT reproduce the bin's process.exit refusal
|
|
27
|
+
// behavior around either resolveBootMode or judgeCoreMigrationFailure —
|
|
28
|
+
// those belong to the standalone server process, not to a library call.
|
|
29
|
+
// Here, any failure (env parsing, client generation, migrations,
|
|
30
|
+
// resolveBootMode itself, or buildApp/buildSetupOnlyApp) simply propagates
|
|
31
|
+
// as a rejected promise for the caller to catch. That IS the right embed
|
|
32
|
+
// behavior: a library function must never call process.exit or decide a
|
|
33
|
+
// host process's fate.
|
|
20
34
|
export async function createApp() {
|
|
21
35
|
const { loadEnvFile } = await import("./config/load-env.js");
|
|
22
36
|
const { normalizeDatabaseUrl } = await import("./setup/normalize-database-url.js");
|
|
@@ -32,8 +46,28 @@ export async function createApp() {
|
|
|
32
46
|
await applyCoreMigrations({ dialect, databaseUrl: process.env.DATABASE_URL, mode: "boot" });
|
|
33
47
|
}
|
|
34
48
|
}
|
|
35
|
-
|
|
36
|
-
|
|
49
|
+
// Deferred until after the preparation above for the same reason index.ts
|
|
50
|
+
// defers it: countAdminUsers builds its own short-lived Prisma client and
|
|
51
|
+
// must see the settled, normalized DATABASE_URL and a self-healed client
|
|
52
|
+
// directory, not whatever was there before loadEnvFile()/
|
|
53
|
+
// normalizeDatabaseUrl() ran. A rejection here (e.g. the admin probe
|
|
54
|
+
// failing or timing out) is not caught — it propagates, same as every
|
|
55
|
+
// other failure in this function.
|
|
56
|
+
const { resolveBootMode, countAdminUsers } = await import("./boot/resolve-boot-mode.js");
|
|
57
|
+
const mode = await resolveBootMode(process.env, countAdminUsers);
|
|
58
|
+
if (mode === "full") {
|
|
59
|
+
const { buildApp } = await import("./app.js");
|
|
60
|
+
return buildApp();
|
|
61
|
+
}
|
|
62
|
+
const { buildSetupOnlyApp } = await import("./setup-app.js");
|
|
63
|
+
return buildSetupOnlyApp({ databaseConfigured: mode === "setup-admin-only" });
|
|
37
64
|
}
|
|
38
65
|
// Alias kept for symmetry with app.ts's internal name.
|
|
39
66
|
export const buildApp = createApp;
|
|
67
|
+
// Review finding I3: spec §4 promises this export. Safe as a static
|
|
68
|
+
// (non-dynamic) re-export — verified sync-admin-bundle.ts imports only
|
|
69
|
+
// node:fs/node:path/node:url, has no top-level await, and touches nothing
|
|
70
|
+
// Prisma-shaped, so unlike createApp's dynamic imports above, importing it
|
|
71
|
+
// eagerly at module load can never construct a Prisma client or otherwise
|
|
72
|
+
// run side effects before a caller is ready for them.
|
|
73
|
+
export { syncAdminBundle } from "./admin/sync-admin-bundle.js";
|