@bloklabs/presets 1.12.0 → 1.14.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @bloklabs/presets
2
2
 
3
- Ready-made [`uploader`](https://blokeditor.com/docs/uploader-api) implementations for [Blok](https://blokeditor.com) — plug in storage you already have instead of hand-writing an upload handler. Five presets ship today: Supabase, S3-compatible storage (presigned URLs), Cloudinary, a generic fetch endpoint, and IndexedDB.
3
+ Ready-made [`uploader`](https://blokeditor.com/docs/uploader-api) implementations for [Blok](https://blokeditor.com). Plug in storage you already have instead of writing an upload handler yourself. Five presets ship today: Supabase, S3-compatible storage (presigned URLs), Cloudinary, a generic fetch endpoint, and IndexedDB.
4
4
 
5
5
  ## Install
6
6
 
@@ -10,7 +10,7 @@ npm install @bloklabs/presets @bloklabs/core
10
10
 
11
11
  ## What a preset is
12
12
 
13
- Blok's `uploader` config option takes an object shaped like this — store the file, return where it landed:
13
+ Blok's `uploader` config option takes an object shaped like this. Store the file, return where it landed:
14
14
 
15
15
  ```ts
16
16
  import { Blok } from '@bloklabs/core';
@@ -26,7 +26,7 @@ new Blok({
26
26
  });
27
27
  ```
28
28
 
29
- Without an `uploadByFile`, an uploaded asset becomes a `blob:` URL that doesn't survive a reload — most projects need to configure one. Each preset in this package is a ready-made implementation of that shape for one storage backend, so you import a preset instead of writing `uploadByFile` yourself.
29
+ Without an `uploadByFile`, an uploaded asset becomes a `blob:` URL. That URL does not survive a reload, so most projects need to configure one. Each preset in this package implements that shape for one storage backend. Import a preset instead of writing `uploadByFile` yourself.
30
30
 
31
31
  ## Which preset, and what it needs
32
32
 
@@ -38,7 +38,7 @@ Without an `uploadByFile`, an uploaded asset becomes a `blob:` URL that doesn't
38
38
  | `cloudinaryStorage` | **Yes** — Cloudinary fetches it itself | An **unsigned** upload preset in the Cloudinary dashboard |
39
39
  | `indexedDBStorage` | No | Nothing — see the warning below |
40
40
 
41
- Only `fetchStorage` and `cloudinaryStorage` can turn a pasted remote URL into a re-hosted copy: both have something that fetches the URL on the server side (your endpoint, or Cloudinary's). The other three cannot — a browser is not allowed to fetch a third-party URL and re-upload it itself. Their `uploadByUrl` is deliberately left undefined rather than faked, so Blok falls back to its documented behavior and stores the pasted URL verbatim.
41
+ Only `fetchStorage` and `cloudinaryStorage` can turn a pasted remote URL into a re-hosted copy. Both have something that fetches the URL on the server side, either your endpoint or Cloudinary's. The other three cannot. A browser is not allowed to fetch a third-party URL and re-upload it itself. Their `uploadByUrl` is deliberately left undefined rather than faked. Blok then falls back to its documented behavior and stores the pasted URL verbatim.
42
42
 
43
43
  ### fetchStorage
44
44
 
@@ -48,7 +48,7 @@ import { fetchStorage } from '@bloklabs/presets';
48
48
  new Blok({ holder: 'editor', uploader: fetchStorage({ baseUrl: 'https://api.myapp.com' }) });
49
49
  ```
50
50
 
51
- Your own backend does the actual storing; this preset just calls it. It must answer `POST {baseUrl}/upload` (multipart) and `POST {baseUrl}/upload-by-url` (`{ url }` JSON), both returning `{ url, fileName? }`. The URL you receive on `/upload-by-url` is user-supplied — validate it server-side and block requests to internal/private addresses before fetching it, or the endpoint becomes an SSRF vector.
51
+ Your own backend does the actual storing. This preset only calls it. The backend must answer `POST {baseUrl}/upload` (multipart) and `POST {baseUrl}/upload-by-url` (`{ url }` JSON). Both return `{ url, fileName? }`. The URL you receive on `/upload-by-url` is user-supplied. Validate it server-side and block requests to internal or private addresses before fetching. Otherwise the endpoint becomes an SSRF vector.
52
52
 
53
53
  ### supabaseStorage
54
54
 
@@ -60,7 +60,7 @@ const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
60
60
  new Blok({ holder: 'editor', uploader: supabaseStorage(supabase, { bucket: 'blok' }) });
61
61
  ```
62
62
 
63
- Create the bucket first (default name `"blok"`), make it public or add an anon `SELECT` policy — this preset returns `getPublicUrl()` directly, so an unreadable object is a broken image — and add an `INSERT` policy for whichever role the client authenticates as.
63
+ Create the bucket first (default name `"blok"`). Make it public or add an anon `SELECT` policy. This preset returns `getPublicUrl()` directly, so an unreadable object is a broken image. Then add an `INSERT` policy for whichever role the client authenticates as.
64
64
 
65
65
  ### presignedStorage
66
66
 
@@ -70,7 +70,7 @@ import { presignedStorage } from '@bloklabs/presets';
70
70
  new Blok({ holder: 'editor', uploader: presignedStorage({ sign: (request) => api.sign(request) }) });
71
71
  ```
72
72
 
73
- For S3, R2, MinIO, or any S3-compatible store. `sign` is your function: it gets `{ fileName, mimeType, size, kind }` and returns a presigned `{ uploadUrl, publicUrl, headers? }` from your backend. The browser then `PUT`s the file straight to `uploadUrl`, so the bucket needs a CORS rule allowing `PUT` (and `Content-Type`) from your app's origin — that CORS rule is the part that actually blocks people.
73
+ For S3, R2, MinIO, or any S3-compatible store. `sign` is your function. It gets `{ fileName, mimeType, size, kind }` and returns a presigned `{ uploadUrl, publicUrl, headers? }` from your backend. The browser then `PUT`s the file straight to `uploadUrl`. So the bucket needs a CORS rule allowing `PUT` (and `Content-Type`) from your app's origin. That CORS rule is the part that actually blocks people.
74
74
 
75
75
  ### cloudinaryStorage
76
76
 
@@ -83,7 +83,7 @@ new Blok({
83
83
  });
84
84
  ```
85
85
 
86
- `uploadPreset` must be **unsigned** (Cloudinary dashboard → Settings → Upload → Upload presets). A signed preset needs a server to sign the request, which defeats the point of a no-backend preset.
86
+ `uploadPreset` must be **unsigned** (Cloudinary dashboard → Settings → Upload → Upload presets). A signed preset needs a server to sign the request. That defeats the point of a preset with no backend.
87
87
 
88
88
  ### indexedDBStorage
89
89
 
@@ -96,7 +96,15 @@ new Blok({ holder: 'editor', uploader: indexedDBStorage() });
96
96
  const displayUrl = await resolveBlokObjectUrl(asset.url);
97
97
  ```
98
98
 
99
- **Not for production.** This preset stores uploaded bytes in the visitor's own browser via IndexedDB. They are gone on another device, in another browser, or the moment the user clears site data — nothing is actually shared or backed up. It exists because Blok's built-in fallback (a `blob:` URL) doesn't even survive a page reload, which makes local demos and prototypes look broken before you've wired up real storage. `uploadByFile` returns a `blok:asset/…` reference, not a directly usable URL — resolve it with `resolveBlokObjectUrl` wherever you render an uploaded asset. If you pass a custom `dbName` to `indexedDBStorage()`, pass the same `dbName` to `resolveBlokObjectUrl(url, { dbName })` — a mismatch resolves to `null` with no error, not a thrown exception. Each call to `resolveBlokObjectUrl` mints a fresh `blob:` URL — nothing is cached, and nothing is revoked. Call `URL.revokeObjectURL()` yourself once a displayed asset is no longer needed, or a view that re-renders it repeatedly will leak memory.
99
+ **Not for production.** This preset stores uploaded bytes in the visitor's own browser via IndexedDB. They are gone on another device, in another browser, or the moment the user clears site data. Nothing is actually shared or backed up.
100
+
101
+ It exists because Blok's built-in fallback (a `blob:` URL) does not even survive a page reload. Before you wire up real storage, that makes local demos and prototypes look broken.
102
+
103
+ `uploadByFile` returns a `blok:asset/…` reference, not a directly usable URL. Resolve it with `resolveBlokObjectUrl` wherever you render an uploaded asset.
104
+
105
+ If you pass a custom `dbName` to `indexedDBStorage()`, pass the same `dbName` to `resolveBlokObjectUrl(url, { dbName })`. A mismatch resolves to `null` with no error, not a thrown exception.
106
+
107
+ Each call to `resolveBlokObjectUrl` mints a fresh `blob:` URL. Nothing is cached, and nothing is revoked. Call `URL.revokeObjectURL()` yourself once a displayed asset is no longer needed. Otherwise a view that re-renders it repeatedly will leak memory.
100
108
 
101
109
  ## Docs
102
110
 
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});function e(e){return new Promise((t,n)=>{var r;let i=new XMLHttpRequest;i.open(e.method,e.url);for(let[t,n]of Object.entries((r=e.headers)==null?{}:r))i.setRequestHeader(t,n);e.onProgress&&(i.upload.onprogress=t=>{var n;!t.lengthComputable||t.total===0||(n=e.onProgress)==null||n.call(e,Math.round(t.loaded/t.total*100))}),i.onload=()=>t({status:i.status,text:i.responseText}),i.onerror=()=>n(Error(`Upload failed: network error`)),i.send(e.body)})}function t(e){"@babel/helpers - typeof";return t=typeof Symbol==`function`&&typeof Symbol.iterator==`symbol`?function(e){return typeof e}:function(e){return e&&typeof Symbol==`function`&&e.constructor===Symbol&&e!==Symbol.prototype?`symbol`:typeof e},t(e)}function n(e,n){if(t(e)!=`object`||!e)return e;var r=e[Symbol.toPrimitive];if(r!==void 0){var i=r.call(e,n||`default`);if(t(i)!=`object`)return i;throw TypeError(`@@toPrimitive must return a primitive value.`)}return(n===`string`?String:Number)(e)}function r(e){var r=n(e,`string`);return t(r)==`symbol`?r:r+``}function i(e,t,n){return(t=r(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function a(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),n.push.apply(n,r)}return n}function o(e){for(var t=1;t<arguments.length;t++){var n=arguments[t]==null?{}:arguments[t];t%2?a(Object(n),!0).forEach(function(t){i(e,t,n[t])}):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):a(Object(n)).forEach(function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))})}return e}function s(t){var n;let r=t.baseUrl.replace(/\/+$/,``),i=(n=t.field)==null?`file`:n,a=async()=>{var e;return typeof t.headers==`function`?t.headers():(e=t.headers)==null?{}:e};return{async uploadByFile(t,n){let o=new FormData;o.append(i,t);let{status:s,text:l}=await e({method:`POST`,url:`${r}/upload`,body:o,headers:await a(),onProgress:n.onProgress});if(s<200||s>299)throw Error(`Fetch endpoint upload failed with status ${s}`);return c(l,t.name)},async uploadByUrl(e,t){let n=await fetch(`${r}/upload-by-url`,{method:`POST`,headers:o(o({},await a()),{},{"Content-Type":`application/json`}),body:JSON.stringify({url:e})});if(!n.ok)throw Error(`Fetch endpoint upload failed with status ${n.status}`);return u(l(await n.text()))}}}function c(e,t){var n;let r=u(l(e));return{url:r.url,fileName:(n=r.fileName)==null?t:n}}function l(e){try{return JSON.parse(e)}catch(e){throw Error(`Upload failed: the endpoint returned a malformed response`)}}function u(e){if(typeof e!=`object`||!e||typeof e.url!=`string`)throw Error(`Upload failed: the endpoint returned no url`);let t=e;return{url:t.url,fileName:typeof t.fileName==`string`?t.fileName:void 0}}function d(e,t={}){let n=e=>{var n;return typeof t.bucket==`function`?t.bucket(e):(n=t.bucket)==null?`blok`:n};return{async uploadByFile(r,i){var a,o;let s=(a=(o=t.path)==null?void 0:o.call(t,r,i))==null?f(r.name):a,{data:c,error:l}=await e.storage.from(n(i.kind)).upload(s,r,{contentType:r.type||void 0,upsert:!1});if(l!==null||c===null){var u;throw Error(`Supabase upload failed: ${(u=l==null?void 0:l.message)==null?`unknown error`:u}`)}let{data:d}=e.storage.from(n(i.kind)).getPublicUrl(c.path);return{url:d.publicUrl,fileName:r.name}}}}function f(e){let t=/\.[a-z0-9]+$/i.exec(e),n=t?t[0].toLowerCase():``;return`${crypto.randomUUID()}${n}`}function p(t){return{async uploadByFile(n,r){let i=await t.sign({fileName:n.name,mimeType:n.type,size:n.size,kind:r.kind}),{status:a}=await e({method:`PUT`,url:i.uploadUrl,body:n,headers:o(o({},i.headers),{},{"Content-Type":n.type||`application/octet-stream`}),onProgress:r.onProgress});if(a<200||a>299)throw Error(`Presigned upload failed with status ${a}`);return{url:i.publicUrl,fileName:n.name}}}}var m={image:`image`,video:`video`,audio:`video`,file:`raw`};function h(t){let n=e=>{var n;return`https://api.cloudinary.com/v1_1/${t.cloudName}/${(n=m[e])==null?`raw`:n}/upload`},r=async(r,i,a)=>{let o=new FormData;o.append(`file`,i),o.append(`upload_preset`,t.uploadPreset),t.folder!==void 0&&o.append(`folder`,t.folder);let{status:s,text:c}=await e({method:`POST`,url:n(r),body:o,onProgress:a});return g(c,s,typeof i==`string`?void 0:i.name)};return{uploadByFile:(e,t)=>r(t.kind,e,t.onProgress),uploadByUrl:(e,t)=>r(t.kind,e,t.onProgress)}}function g(e,t,n){let r=_(e,t);if(typeof r.secure_url!=`string`){var i;let e=r.error;throw Error(`Cloudinary upload failed: ${(i=e==null?void 0:e.message)==null?`status ${t}`:i}`)}return{url:r.secure_url,fileName:n}}function _(e,t){try{return JSON.parse(e)}catch(e){throw Error(`Cloudinary upload failed with status ${t}`)}}var v=`blok-assets`,y=`assets`,b=`blok:asset/`;function x(e){return new Promise((t,n)=>{let r=indexedDB.open(e,1);r.onupgradeneeded=()=>{r.result.createObjectStore(y)},r.onsuccess=()=>t(r.result),r.onerror=()=>{var e;return n((e=r.error)==null?Error(`IndexedDB open failed`):e)}})}function S(e,t,n){return new Promise((r,i)=>{let a=n(e.transaction(y,t).objectStore(y));a.onsuccess=()=>r(a.result),a.onerror=()=>{var e;return i((e=a.error)==null?Error(`IndexedDB request failed`):e)}})}function C(e={}){var t;let n=(t=e.dbName)==null?v:t;return{async uploadByFile(e,t){let r=crypto.randomUUID(),i=await x(n);try{await S(i,`readwrite`,t=>t.put({blob:e,name:e.name,type:e.type},r))}finally{i.close()}return{url:`${b}${r}`,fileName:e.name}}}}async function w(e,t={}){var n;if(!e.startsWith(b))return e;let r=await T(await x((n=t.dbName)==null?v:n),e.slice(11));return r?URL.createObjectURL(r.blob):null}function T(e,t){return S(e,`readonly`,e=>e.get(t)).finally(()=>e.close())}exports.cloudinaryStorage=h,exports.fetchStorage=s,exports.indexedDBStorage=C,exports.presignedStorage=p,exports.resolveBlokObjectUrl=w,exports.supabaseStorage=d;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});function e(e){return new Promise((t,n)=>{var r;let i=new XMLHttpRequest;i.open(e.method,e.url);for(let[t,n]of Object.entries((r=e.headers)==null?{}:r))i.setRequestHeader(t,n);e.onProgress&&(i.upload.onprogress=t=>{var n;!t.lengthComputable||t.total===0||(n=e.onProgress)==null||n.call(e,Math.round(t.loaded/t.total*100))}),i.onload=()=>t({status:i.status,text:i.responseText}),i.onerror=()=>n(Error(`Upload failed: network error`)),i.send(e.body)})}function t(e){"@babel/helpers - typeof";return t=typeof Symbol==`function`&&typeof Symbol.iterator==`symbol`?function(e){return typeof e}:function(e){return e&&typeof Symbol==`function`&&e.constructor===Symbol&&e!==Symbol.prototype?`symbol`:typeof e},t(e)}function n(e,n){if(t(e)!=`object`||!e)return e;var r=e[Symbol.toPrimitive];if(r!==void 0){var i=r.call(e,n||`default`);if(t(i)!=`object`)return i;throw TypeError(`@@toPrimitive must return a primitive value.`)}return(n===`string`?String:Number)(e)}function r(e){var r=n(e,`string`);return t(r)==`symbol`?r:r+``}function i(e,t,n){return(t=r(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function a(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),n.push.apply(n,r)}return n}function o(e){for(var t=1;t<arguments.length;t++){var n=arguments[t]==null?{}:arguments[t];t%2?a(Object(n),!0).forEach(function(t){i(e,t,n[t])}):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):a(Object(n)).forEach(function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))})}return e}function s(e){let t=Array.from(e).reduce((e,t)=>t===`/`?e+1:0,0);return e.slice(0,e.length-t)}function c(t){var n;let r=s(t.baseUrl),i=(n=t.field)==null?`file`:n,a=async()=>{var e;return typeof t.headers==`function`?t.headers():(e=t.headers)==null?{}:e};return{async uploadByFile(t,n){let o=new FormData;o.append(i,t);let{status:s,text:c}=await e({method:`POST`,url:`${r}/upload`,body:o,headers:await a(),onProgress:n.onProgress});if(s<200||s>299)throw Error(`Fetch endpoint upload failed with status ${s}`);return l(c,t.name)},async uploadByUrl(e,t){let n=await fetch(`${r}/upload-by-url`,{method:`POST`,headers:o(o({},await a()),{},{"Content-Type":`application/json`}),body:JSON.stringify({url:e})});if(!n.ok)throw Error(`Fetch endpoint upload failed with status ${n.status}`);return d(u(await n.text()))},async delete(e,t){let n=await fetch(`${r}/delete`,{method:`POST`,headers:o(o({},await a()),{},{"Content-Type":`application/json`}),body:JSON.stringify({url:e})});if(!n.ok)throw Error(`Fetch endpoint delete failed with status ${n.status}`)}}}function l(e,t){var n;let r=d(u(e));return{url:r.url,fileName:(n=r.fileName)==null?t:n}}function u(e){try{return JSON.parse(e)}catch(e){throw Error(`Upload failed: the endpoint returned a malformed response`)}}function d(e){if(typeof e!=`object`||!e||typeof e.url!=`string`)throw Error(`Upload failed: the endpoint returned no url`);let t=e;return{url:t.url,fileName:typeof t.fileName==`string`?t.fileName:void 0}}function f(e){return c(e)}function p(e,t={}){let n=e=>{var n;return typeof t.bucket==`function`?t.bucket(e):(n=t.bucket)==null?`blok`:n};return{async uploadByFile(r,i){var a,o;let s=(a=(o=t.path)==null?void 0:o.call(t,r,i))==null?m(r.name):a,{data:c,error:l}=await e.storage.from(n(i.kind)).upload(s,r,{contentType:r.type||void 0,upsert:!1});if(l!==null||c===null){var u;throw Error(`Supabase upload failed: ${(u=l==null?void 0:l.message)==null?`unknown error`:u}`)}let{data:d}=e.storage.from(n(i.kind)).getPublicUrl(c.path);return{url:d.publicUrl,fileName:r.name}}}}function m(e){let t=/\.[a-z0-9]+$/i.exec(e),n=t?t[0].toLowerCase():``;return`${crypto.randomUUID()}${n}`}function h(t){return{async uploadByFile(n,r){let i=await t.sign({fileName:n.name,mimeType:n.type,size:n.size,kind:r.kind}),{status:a}=await e({method:`PUT`,url:i.uploadUrl,body:n,headers:o(o({},i.headers),{},{"Content-Type":n.type||`application/octet-stream`}),onProgress:r.onProgress});if(a<200||a>299)throw Error(`Presigned upload failed with status ${a}`);return{url:i.publicUrl,fileName:n.name}}}}var g={image:`image`,video:`video`,audio:`video`,file:`raw`};function _(t){let n=e=>{var n;return`https://api.cloudinary.com/v1_1/${t.cloudName}/${(n=g[e])==null?`raw`:n}/upload`},r=async(r,i,a)=>{let o=new FormData;o.append(`file`,i),o.append(`upload_preset`,t.uploadPreset),t.folder!==void 0&&o.append(`folder`,t.folder);let{status:s,text:c}=await e({method:`POST`,url:n(r),body:o,onProgress:a});return v(c,s,typeof i==`string`?void 0:i.name)};return{uploadByFile:(e,t)=>r(t.kind,e,t.onProgress),uploadByUrl:(e,t)=>r(t.kind,e,t.onProgress)}}function v(e,t,n){let r=y(e,t);if(typeof r.secure_url!=`string`){var i;let e=r.error;throw Error(`Cloudinary upload failed: ${(i=e==null?void 0:e.message)==null?`status ${t}`:i}`)}return{url:r.secure_url,fileName:n}}function y(e,t){try{return JSON.parse(e)}catch(e){throw Error(`Cloudinary upload failed with status ${t}`)}}var b=`blok-assets`,x=`assets`,S=`blok:asset/`;function C(e){return new Promise((t,n)=>{let r=indexedDB.open(e,1);r.onupgradeneeded=()=>{r.result.createObjectStore(x)},r.onsuccess=()=>t(r.result),r.onerror=()=>{var e;return n((e=r.error)==null?Error(`IndexedDB open failed`):e)}})}function w(e,t,n){return new Promise((r,i)=>{let a=n(e.transaction(x,t).objectStore(x));a.onsuccess=()=>r(a.result),a.onerror=()=>{var e;return i((e=a.error)==null?Error(`IndexedDB request failed`):e)}})}function T(e={}){var t;let n=(t=e.dbName)==null?b:t;return{async uploadByFile(e,t){let r=crypto.randomUUID(),i=await C(n);try{await w(i,`readwrite`,t=>t.put({blob:e,name:e.name,type:e.type},r))}finally{i.close()}return{url:`${S}${r}`,fileName:e.name}}}}async function E(e,t={}){var n;if(!e.startsWith(S))return e;let r=await D(await C((n=t.dbName)==null?b:n),e.slice(11));return r?URL.createObjectURL(r.blob):null}function D(e,t){return w(e,`readonly`,e=>e.get(t)).finally(()=>e.close())}exports.cloudinaryStorage=_,exports.fetchStorage=f,exports.indexedDBStorage=T,exports.presignedStorage=h,exports.resolveBlokObjectUrl=E,exports.supabaseStorage=p;
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- //#region src/upload-xhr.ts
1
+ //#region ../../src/components/utils/upload-xhr.ts
2
2
  function e(e) {
3
3
  return new Promise((t, n) => {
4
4
  var r;
@@ -15,7 +15,7 @@ function e(e) {
15
15
  });
16
16
  }
17
17
  //#endregion
18
- //#region \0@oxc-project+runtime@0.139.0/helpers/esm/typeof.js
18
+ //#region \0@oxc-project+runtime@0.147.0/helpers/esm/typeof.js
19
19
  function t(e) {
20
20
  "@babel/helpers - typeof";
21
21
  return t = typeof Symbol == "function" && typeof Symbol.iterator == "symbol" ? function(e) {
@@ -25,7 +25,7 @@ function t(e) {
25
25
  }, t(e);
26
26
  }
27
27
  //#endregion
28
- //#region \0@oxc-project+runtime@0.139.0/helpers/esm/toPrimitive.js
28
+ //#region \0@oxc-project+runtime@0.147.0/helpers/esm/toPrimitive.js
29
29
  function n(e, n) {
30
30
  if (t(e) != "object" || !e) return e;
31
31
  var r = e[Symbol.toPrimitive];
@@ -37,13 +37,13 @@ function n(e, n) {
37
37
  return (n === "string" ? String : Number)(e);
38
38
  }
39
39
  //#endregion
40
- //#region \0@oxc-project+runtime@0.139.0/helpers/esm/toPropertyKey.js
40
+ //#region \0@oxc-project+runtime@0.147.0/helpers/esm/toPropertyKey.js
41
41
  function r(e) {
42
42
  var r = n(e, "string");
43
43
  return t(r) == "symbol" ? r : r + "";
44
44
  }
45
45
  //#endregion
46
- //#region \0@oxc-project+runtime@0.139.0/helpers/esm/defineProperty.js
46
+ //#region \0@oxc-project+runtime@0.147.0/helpers/esm/defineProperty.js
47
47
  function i(e, t, n) {
48
48
  return (t = r(t)) in e ? Object.defineProperty(e, t, {
49
49
  value: n,
@@ -53,7 +53,7 @@ function i(e, t, n) {
53
53
  }) : e[t] = n, e;
54
54
  }
55
55
  //#endregion
56
- //#region \0@oxc-project+runtime@0.139.0/helpers/esm/objectSpread2.js
56
+ //#region \0@oxc-project+runtime@0.147.0/helpers/esm/objectSpread2.js
57
57
  function a(e, t) {
58
58
  var n = Object.keys(e);
59
59
  if (Object.getOwnPropertySymbols) {
@@ -76,10 +76,14 @@ function o(e) {
76
76
  return e;
77
77
  }
78
78
  //#endregion
79
- //#region src/fetch-endpoint.ts
80
- function s(t) {
79
+ //#region ../../src/components/utils/fetch-uploader.ts
80
+ function s(e) {
81
+ let t = Array.from(e).reduce((e, t) => t === "/" ? e + 1 : 0, 0);
82
+ return e.slice(0, e.length - t);
83
+ }
84
+ function c(t) {
81
85
  var n;
82
- let r = t.baseUrl.replace(/\/+$/, ""), i = (n = t.field) == null ? "file" : n, a = async () => {
86
+ let r = s(t.baseUrl), i = (n = t.field) == null ? "file" : n, a = async () => {
83
87
  var e;
84
88
  return typeof t.headers == "function" ? t.headers() : (e = t.headers) == null ? {} : e;
85
89
  };
@@ -87,7 +91,7 @@ function s(t) {
87
91
  async uploadByFile(t, n) {
88
92
  let o = new FormData();
89
93
  o.append(i, t);
90
- let { status: s, text: l } = await e({
94
+ let { status: s, text: c } = await e({
91
95
  method: "POST",
92
96
  url: `${r}/upload`,
93
97
  body: o,
@@ -95,7 +99,7 @@ function s(t) {
95
99
  onProgress: n.onProgress
96
100
  });
97
101
  if (s < 200 || s > 299) throw Error(`Fetch endpoint upload failed with status ${s}`);
98
- return c(l, t.name);
102
+ return l(c, t.name);
99
103
  },
100
104
  async uploadByUrl(e, t) {
101
105
  let n = await fetch(`${r}/upload-by-url`, {
@@ -104,26 +108,34 @@ function s(t) {
104
108
  body: JSON.stringify({ url: e })
105
109
  });
106
110
  if (!n.ok) throw Error(`Fetch endpoint upload failed with status ${n.status}`);
107
- return u(l(await n.text()));
111
+ return d(u(await n.text()));
112
+ },
113
+ async delete(e, t) {
114
+ let n = await fetch(`${r}/delete`, {
115
+ method: "POST",
116
+ headers: o(o({}, await a()), {}, { "Content-Type": "application/json" }),
117
+ body: JSON.stringify({ url: e })
118
+ });
119
+ if (!n.ok) throw Error(`Fetch endpoint delete failed with status ${n.status}`);
108
120
  }
109
121
  };
110
122
  }
111
- function c(e, t) {
123
+ function l(e, t) {
112
124
  var n;
113
- let r = u(l(e));
125
+ let r = d(u(e));
114
126
  return {
115
127
  url: r.url,
116
128
  fileName: (n = r.fileName) == null ? t : n
117
129
  };
118
130
  }
119
- function l(e) {
131
+ function u(e) {
120
132
  try {
121
133
  return JSON.parse(e);
122
134
  } catch (e) {
123
135
  throw Error("Upload failed: the endpoint returned a malformed response");
124
136
  }
125
137
  }
126
- function u(e) {
138
+ function d(e) {
127
139
  if (typeof e != "object" || !e || typeof e.url != "string") throw Error("Upload failed: the endpoint returned no url");
128
140
  let t = e;
129
141
  return {
@@ -132,15 +144,20 @@ function u(e) {
132
144
  };
133
145
  }
134
146
  //#endregion
147
+ //#region src/fetch-endpoint.ts
148
+ function f(e) {
149
+ return c(e);
150
+ }
151
+ //#endregion
135
152
  //#region src/supabase.ts
136
- function d(e, t = {}) {
153
+ function p(e, t = {}) {
137
154
  let n = (e) => {
138
155
  var n;
139
156
  return typeof t.bucket == "function" ? t.bucket(e) : (n = t.bucket) == null ? "blok" : n;
140
157
  };
141
158
  return { async uploadByFile(r, i) {
142
159
  var a, o;
143
- let s = (a = (o = t.path) == null ? void 0 : o.call(t, r, i)) == null ? f(r.name) : a, { data: c, error: l } = await e.storage.from(n(i.kind)).upload(s, r, {
160
+ let s = (a = (o = t.path) == null ? void 0 : o.call(t, r, i)) == null ? m(r.name) : a, { data: c, error: l } = await e.storage.from(n(i.kind)).upload(s, r, {
144
161
  contentType: r.type || void 0,
145
162
  upsert: !1
146
163
  });
@@ -155,13 +172,13 @@ function d(e, t = {}) {
155
172
  };
156
173
  } };
157
174
  }
158
- function f(e) {
175
+ function m(e) {
159
176
  let t = /\.[a-z0-9]+$/i.exec(e), n = t ? t[0].toLowerCase() : "";
160
177
  return `${crypto.randomUUID()}${n}`;
161
178
  }
162
179
  //#endregion
163
180
  //#region src/presigned.ts
164
- function p(t) {
181
+ function h(t) {
165
182
  return { async uploadByFile(n, r) {
166
183
  let i = await t.sign({
167
184
  fileName: n.name,
@@ -184,16 +201,16 @@ function p(t) {
184
201
  }
185
202
  //#endregion
186
203
  //#region src/cloudinary.ts
187
- var m = {
204
+ var g = {
188
205
  image: "image",
189
206
  video: "video",
190
207
  audio: "video",
191
208
  file: "raw"
192
209
  };
193
- function h(t) {
210
+ function _(t) {
194
211
  let n = (e) => {
195
212
  var n;
196
- return `https://api.cloudinary.com/v1_1/${t.cloudName}/${(n = m[e]) == null ? "raw" : n}/upload`;
213
+ return `https://api.cloudinary.com/v1_1/${t.cloudName}/${(n = g[e]) == null ? "raw" : n}/upload`;
197
214
  }, r = async (r, i, a) => {
198
215
  let o = new FormData();
199
216
  o.append("file", i), o.append("upload_preset", t.uploadPreset), t.folder !== void 0 && o.append("folder", t.folder);
@@ -203,15 +220,15 @@ function h(t) {
203
220
  body: o,
204
221
  onProgress: a
205
222
  });
206
- return g(c, s, typeof i == "string" ? void 0 : i.name);
223
+ return v(c, s, typeof i == "string" ? void 0 : i.name);
207
224
  };
208
225
  return {
209
226
  uploadByFile: (e, t) => r(t.kind, e, t.onProgress),
210
227
  uploadByUrl: (e, t) => r(t.kind, e, t.onProgress)
211
228
  };
212
229
  }
213
- function g(e, t, n) {
214
- let r = _(e, t);
230
+ function v(e, t, n) {
231
+ let r = y(e, t);
215
232
  if (typeof r.secure_url != "string") {
216
233
  var i;
217
234
  let e = r.error;
@@ -222,7 +239,7 @@ function g(e, t, n) {
222
239
  fileName: n
223
240
  };
224
241
  }
225
- function _(e, t) {
242
+ function y(e, t) {
226
243
  try {
227
244
  return JSON.parse(e);
228
245
  } catch (e) {
@@ -231,34 +248,34 @@ function _(e, t) {
231
248
  }
232
249
  //#endregion
233
250
  //#region src/indexeddb.ts
234
- var v = "blok-assets", y = "assets", b = "blok:asset/";
235
- function x(e) {
251
+ var b = "blok-assets", x = "assets", S = "blok:asset/";
252
+ function C(e) {
236
253
  return new Promise((t, n) => {
237
254
  let r = indexedDB.open(e, 1);
238
255
  r.onupgradeneeded = () => {
239
- r.result.createObjectStore(y);
256
+ r.result.createObjectStore(x);
240
257
  }, r.onsuccess = () => t(r.result), r.onerror = () => {
241
258
  var e;
242
259
  return n((e = r.error) == null ? /* @__PURE__ */ Error("IndexedDB open failed") : e);
243
260
  };
244
261
  });
245
262
  }
246
- function S(e, t, n) {
263
+ function w(e, t, n) {
247
264
  return new Promise((r, i) => {
248
- let a = n(e.transaction(y, t).objectStore(y));
265
+ let a = n(e.transaction(x, t).objectStore(x));
249
266
  a.onsuccess = () => r(a.result), a.onerror = () => {
250
267
  var e;
251
268
  return i((e = a.error) == null ? /* @__PURE__ */ Error("IndexedDB request failed") : e);
252
269
  };
253
270
  });
254
271
  }
255
- function C(e = {}) {
272
+ function T(e = {}) {
256
273
  var t;
257
- let n = (t = e.dbName) == null ? v : t;
274
+ let n = (t = e.dbName) == null ? b : t;
258
275
  return { async uploadByFile(e, t) {
259
- let r = crypto.randomUUID(), i = await x(n);
276
+ let r = crypto.randomUUID(), i = await C(n);
260
277
  try {
261
- await S(i, "readwrite", (t) => t.put({
278
+ await w(i, "readwrite", (t) => t.put({
262
279
  blob: e,
263
280
  name: e.name,
264
281
  type: e.type
@@ -267,19 +284,19 @@ function C(e = {}) {
267
284
  i.close();
268
285
  }
269
286
  return {
270
- url: `${b}${r}`,
287
+ url: `${S}${r}`,
271
288
  fileName: e.name
272
289
  };
273
290
  } };
274
291
  }
275
- async function w(e, t = {}) {
292
+ async function E(e, t = {}) {
276
293
  var n;
277
- if (!e.startsWith(b)) return e;
278
- let r = await T(await x((n = t.dbName) == null ? v : n), e.slice(11));
294
+ if (!e.startsWith(S)) return e;
295
+ let r = await D(await C((n = t.dbName) == null ? b : n), e.slice(11));
279
296
  return r ? URL.createObjectURL(r.blob) : null;
280
297
  }
281
- function T(e, t) {
282
- return S(e, "readonly", (e) => e.get(t)).finally(() => e.close());
298
+ function D(e, t) {
299
+ return w(e, "readonly", (e) => e.get(t)).finally(() => e.close());
283
300
  }
284
301
  //#endregion
285
- export { h as cloudinaryStorage, s as fetchStorage, C as indexedDBStorage, p as presignedStorage, w as resolveBlokObjectUrl, d as supabaseStorage };
302
+ export { _ as cloudinaryStorage, f as fetchStorage, T as indexedDBStorage, h as presignedStorage, E as resolveBlokObjectUrl, p as supabaseStorage };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bloklabs/presets",
3
- "version": "1.12.0",
3
+ "version": "1.14.0",
4
4
  "description": "Storage presets for Blok — ready-made uploaders for Supabase, S3-compatible storage, Cloudinary, and IndexedDB.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "JackUait",
package/types/index.d.ts CHANGED
@@ -45,14 +45,29 @@ interface UploadContext {
45
45
  }
46
46
 
47
47
  /**
48
- * Editor-level uploader, keyed by asset kind rather than by tool. Both
49
- * methods are optional and resolved independently.
48
+ * Describes a single deletion request.
49
+ *
50
+ * The same `kind`/`tool` pair {@link UploadContext} carries, minus progress —
51
+ * a deletion has nothing to report but its outcome.
52
+ */
53
+ interface DeleteContext {
54
+ /** Kind of asset being deleted. */
55
+ kind: AssetKind;
56
+ /** Name of the block tool that owned the asset, when known. */
57
+ tool?: string;
58
+ }
59
+
60
+ /**
61
+ * Editor-level uploader, keyed by asset kind rather than by tool. Every
62
+ * method is optional and resolved independently.
50
63
  */
51
64
  interface BlokUploader {
52
65
  /** Store a file chosen by the user and return its public URL. */
53
66
  uploadByFile?(file: File, ctx: UploadContext): Promise<UploadedAsset>;
54
67
  /** Re-host an asset the user supplied by URL, and return the stored URL. */
55
68
  uploadByUrl?(url: string, ctx: UploadContext): Promise<UploadedAsset>;
69
+ /** Delete a stored asset by the URL this uploader returned for it. */
70
+ delete?(url: string, ctx: DeleteContext): Promise<void>;
56
71
  }
57
72
 
58
73
  export interface FetchStorageOptions {