@bloklabs/presets 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -0
- package/dist/index.cjs +1 -0
- package/dist/index.mjs +285 -0
- package/package.json +54 -0
- package/types/index.d.ts +175 -0
package/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# @bloklabs/presets
|
|
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.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @bloklabs/presets @bloklabs/core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## What a preset is
|
|
12
|
+
|
|
13
|
+
Blok's `uploader` config option takes an object shaped like this — store the file, return where it landed:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { Blok } from '@bloklabs/core';
|
|
17
|
+
|
|
18
|
+
new Blok({
|
|
19
|
+
holder: 'editor',
|
|
20
|
+
uploader: {
|
|
21
|
+
async uploadByFile(file, { kind }) {
|
|
22
|
+
const res = await fetch(`/upload/${kind}`, { method: 'POST', body: file });
|
|
23
|
+
return { url: (await res.json()).url };
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
```
|
|
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.
|
|
30
|
+
|
|
31
|
+
## Which preset, and what it needs
|
|
32
|
+
|
|
33
|
+
| Preset | Re-hosts a pasted URL? | Needs on the storage side |
|
|
34
|
+
| --- | --- | --- |
|
|
35
|
+
| `fetchStorage` | **Yes** — your endpoint fetches it server-side | A backend answering `POST /upload` and `POST /upload-by-url` |
|
|
36
|
+
| `supabaseStorage` | No | A Storage bucket, made public (or an anon SELECT policy), plus an INSERT policy |
|
|
37
|
+
| `presignedStorage` | No | A backend that mints presigned PUT URLs, and a CORS rule on the bucket allowing PUT |
|
|
38
|
+
| `cloudinaryStorage` | **Yes** — Cloudinary fetches it itself | An **unsigned** upload preset in the Cloudinary dashboard |
|
|
39
|
+
| `indexedDBStorage` | No | Nothing — see the warning below |
|
|
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.
|
|
42
|
+
|
|
43
|
+
### fetchStorage
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { fetchStorage } from '@bloklabs/presets';
|
|
47
|
+
|
|
48
|
+
new Blok({ holder: 'editor', uploader: fetchStorage({ baseUrl: 'https://api.myapp.com' }) });
|
|
49
|
+
```
|
|
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.
|
|
52
|
+
|
|
53
|
+
### supabaseStorage
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { createClient } from '@supabase/supabase-js';
|
|
57
|
+
import { supabaseStorage } from '@bloklabs/presets';
|
|
58
|
+
|
|
59
|
+
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
|
|
60
|
+
new Blok({ holder: 'editor', uploader: supabaseStorage(supabase, { bucket: 'blok' }) });
|
|
61
|
+
```
|
|
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.
|
|
64
|
+
|
|
65
|
+
### presignedStorage
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { presignedStorage } from '@bloklabs/presets';
|
|
69
|
+
|
|
70
|
+
new Blok({ holder: 'editor', uploader: presignedStorage({ sign: (request) => api.sign(request) }) });
|
|
71
|
+
```
|
|
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.
|
|
74
|
+
|
|
75
|
+
### cloudinaryStorage
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import { cloudinaryStorage } from '@bloklabs/presets';
|
|
79
|
+
|
|
80
|
+
new Blok({
|
|
81
|
+
holder: 'editor',
|
|
82
|
+
uploader: cloudinaryStorage({ cloudName: 'my-cloud', uploadPreset: 'blok-unsigned' }),
|
|
83
|
+
});
|
|
84
|
+
```
|
|
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.
|
|
87
|
+
|
|
88
|
+
### indexedDBStorage
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { indexedDBStorage, resolveBlokObjectUrl } from '@bloklabs/presets';
|
|
92
|
+
|
|
93
|
+
new Blok({ holder: 'editor', uploader: indexedDBStorage() });
|
|
94
|
+
|
|
95
|
+
// Wherever you render a stored asset's url, e.g. an <img src>:
|
|
96
|
+
const displayUrl = await resolveBlokObjectUrl(asset.url);
|
|
97
|
+
```
|
|
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.
|
|
100
|
+
|
|
101
|
+
## Docs
|
|
102
|
+
|
|
103
|
+
- [Uploader API reference](https://blokeditor.com/docs/uploader-api)
|
|
104
|
+
- [Storage presets guide](https://blokeditor.com/presets)
|
|
105
|
+
|
|
106
|
+
Licensed under Apache-2.0.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +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;
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
//#region src/upload-xhr.ts
|
|
2
|
+
function e(e) {
|
|
3
|
+
return new Promise((t, n) => {
|
|
4
|
+
var r;
|
|
5
|
+
let i = new XMLHttpRequest();
|
|
6
|
+
i.open(e.method, e.url);
|
|
7
|
+
for (let [t, n] of Object.entries((r = e.headers) == null ? {} : r)) i.setRequestHeader(t, n);
|
|
8
|
+
e.onProgress && (i.upload.onprogress = (t) => {
|
|
9
|
+
var n;
|
|
10
|
+
!t.lengthComputable || t.total === 0 || (n = e.onProgress) == null || n.call(e, Math.round(t.loaded / t.total * 100));
|
|
11
|
+
}), i.onload = () => t({
|
|
12
|
+
status: i.status,
|
|
13
|
+
text: i.responseText
|
|
14
|
+
}), i.onerror = () => n(/* @__PURE__ */ Error("Upload failed: network error")), i.send(e.body);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region \0@oxc-project+runtime@0.139.0/helpers/esm/typeof.js
|
|
19
|
+
function t(e) {
|
|
20
|
+
"@babel/helpers - typeof";
|
|
21
|
+
return t = typeof Symbol == "function" && typeof Symbol.iterator == "symbol" ? function(e) {
|
|
22
|
+
return typeof e;
|
|
23
|
+
} : function(e) {
|
|
24
|
+
return e && typeof Symbol == "function" && e.constructor === Symbol && e !== Symbol.prototype ? "symbol" : typeof e;
|
|
25
|
+
}, t(e);
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region \0@oxc-project+runtime@0.139.0/helpers/esm/toPrimitive.js
|
|
29
|
+
function n(e, n) {
|
|
30
|
+
if (t(e) != "object" || !e) return e;
|
|
31
|
+
var r = e[Symbol.toPrimitive];
|
|
32
|
+
if (r !== void 0) {
|
|
33
|
+
var i = r.call(e, n || "default");
|
|
34
|
+
if (t(i) != "object") return i;
|
|
35
|
+
throw TypeError("@@toPrimitive must return a primitive value.");
|
|
36
|
+
}
|
|
37
|
+
return (n === "string" ? String : Number)(e);
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region \0@oxc-project+runtime@0.139.0/helpers/esm/toPropertyKey.js
|
|
41
|
+
function r(e) {
|
|
42
|
+
var r = n(e, "string");
|
|
43
|
+
return t(r) == "symbol" ? r : r + "";
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region \0@oxc-project+runtime@0.139.0/helpers/esm/defineProperty.js
|
|
47
|
+
function i(e, t, n) {
|
|
48
|
+
return (t = r(t)) in e ? Object.defineProperty(e, t, {
|
|
49
|
+
value: n,
|
|
50
|
+
enumerable: !0,
|
|
51
|
+
configurable: !0,
|
|
52
|
+
writable: !0
|
|
53
|
+
}) : e[t] = n, e;
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region \0@oxc-project+runtime@0.139.0/helpers/esm/objectSpread2.js
|
|
57
|
+
function a(e, t) {
|
|
58
|
+
var n = Object.keys(e);
|
|
59
|
+
if (Object.getOwnPropertySymbols) {
|
|
60
|
+
var r = Object.getOwnPropertySymbols(e);
|
|
61
|
+
t && (r = r.filter(function(t) {
|
|
62
|
+
return Object.getOwnPropertyDescriptor(e, t).enumerable;
|
|
63
|
+
})), n.push.apply(n, r);
|
|
64
|
+
}
|
|
65
|
+
return n;
|
|
66
|
+
}
|
|
67
|
+
function o(e) {
|
|
68
|
+
for (var t = 1; t < arguments.length; t++) {
|
|
69
|
+
var n = arguments[t] == null ? {} : arguments[t];
|
|
70
|
+
t % 2 ? a(Object(n), !0).forEach(function(t) {
|
|
71
|
+
i(e, t, n[t]);
|
|
72
|
+
}) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n)) : a(Object(n)).forEach(function(t) {
|
|
73
|
+
Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return e;
|
|
77
|
+
}
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region src/fetch-endpoint.ts
|
|
80
|
+
function s(t) {
|
|
81
|
+
var n;
|
|
82
|
+
let r = t.baseUrl.replace(/\/+$/, ""), i = (n = t.field) == null ? "file" : n, a = async () => {
|
|
83
|
+
var e;
|
|
84
|
+
return typeof t.headers == "function" ? t.headers() : (e = t.headers) == null ? {} : e;
|
|
85
|
+
};
|
|
86
|
+
return {
|
|
87
|
+
async uploadByFile(t, n) {
|
|
88
|
+
let o = new FormData();
|
|
89
|
+
o.append(i, t);
|
|
90
|
+
let { status: s, text: l } = await e({
|
|
91
|
+
method: "POST",
|
|
92
|
+
url: `${r}/upload`,
|
|
93
|
+
body: o,
|
|
94
|
+
headers: await a(),
|
|
95
|
+
onProgress: n.onProgress
|
|
96
|
+
});
|
|
97
|
+
if (s < 200 || s > 299) throw Error(`Fetch endpoint upload failed with status ${s}`);
|
|
98
|
+
return c(l, t.name);
|
|
99
|
+
},
|
|
100
|
+
async uploadByUrl(e, t) {
|
|
101
|
+
let n = await fetch(`${r}/upload-by-url`, {
|
|
102
|
+
method: "POST",
|
|
103
|
+
headers: o(o({}, await a()), {}, { "Content-Type": "application/json" }),
|
|
104
|
+
body: JSON.stringify({ url: e })
|
|
105
|
+
});
|
|
106
|
+
if (!n.ok) throw Error(`Fetch endpoint upload failed with status ${n.status}`);
|
|
107
|
+
return u(l(await n.text()));
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function c(e, t) {
|
|
112
|
+
var n;
|
|
113
|
+
let r = u(l(e));
|
|
114
|
+
return {
|
|
115
|
+
url: r.url,
|
|
116
|
+
fileName: (n = r.fileName) == null ? t : n
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function l(e) {
|
|
120
|
+
try {
|
|
121
|
+
return JSON.parse(e);
|
|
122
|
+
} catch (e) {
|
|
123
|
+
throw Error("Upload failed: the endpoint returned a malformed response");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function u(e) {
|
|
127
|
+
if (typeof e != "object" || !e || typeof e.url != "string") throw Error("Upload failed: the endpoint returned no url");
|
|
128
|
+
let t = e;
|
|
129
|
+
return {
|
|
130
|
+
url: t.url,
|
|
131
|
+
fileName: typeof t.fileName == "string" ? t.fileName : void 0
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
//#endregion
|
|
135
|
+
//#region src/supabase.ts
|
|
136
|
+
function d(e, t = {}) {
|
|
137
|
+
let n = (e) => {
|
|
138
|
+
var n;
|
|
139
|
+
return typeof t.bucket == "function" ? t.bucket(e) : (n = t.bucket) == null ? "blok" : n;
|
|
140
|
+
};
|
|
141
|
+
return { async uploadByFile(r, i) {
|
|
142
|
+
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, {
|
|
144
|
+
contentType: r.type || void 0,
|
|
145
|
+
upsert: !1
|
|
146
|
+
});
|
|
147
|
+
if (l !== null || c === null) {
|
|
148
|
+
var u;
|
|
149
|
+
throw Error(`Supabase upload failed: ${(u = l == null ? void 0 : l.message) == null ? "unknown error" : u}`);
|
|
150
|
+
}
|
|
151
|
+
let { data: d } = e.storage.from(n(i.kind)).getPublicUrl(c.path);
|
|
152
|
+
return {
|
|
153
|
+
url: d.publicUrl,
|
|
154
|
+
fileName: r.name
|
|
155
|
+
};
|
|
156
|
+
} };
|
|
157
|
+
}
|
|
158
|
+
function f(e) {
|
|
159
|
+
let t = /\.[a-z0-9]+$/i.exec(e), n = t ? t[0].toLowerCase() : "";
|
|
160
|
+
return `${crypto.randomUUID()}${n}`;
|
|
161
|
+
}
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region src/presigned.ts
|
|
164
|
+
function p(t) {
|
|
165
|
+
return { async uploadByFile(n, r) {
|
|
166
|
+
let i = await t.sign({
|
|
167
|
+
fileName: n.name,
|
|
168
|
+
mimeType: n.type,
|
|
169
|
+
size: n.size,
|
|
170
|
+
kind: r.kind
|
|
171
|
+
}), { status: a } = await e({
|
|
172
|
+
method: "PUT",
|
|
173
|
+
url: i.uploadUrl,
|
|
174
|
+
body: n,
|
|
175
|
+
headers: o(o({}, i.headers), {}, { "Content-Type": n.type || "application/octet-stream" }),
|
|
176
|
+
onProgress: r.onProgress
|
|
177
|
+
});
|
|
178
|
+
if (a < 200 || a > 299) throw Error(`Presigned upload failed with status ${a}`);
|
|
179
|
+
return {
|
|
180
|
+
url: i.publicUrl,
|
|
181
|
+
fileName: n.name
|
|
182
|
+
};
|
|
183
|
+
} };
|
|
184
|
+
}
|
|
185
|
+
//#endregion
|
|
186
|
+
//#region src/cloudinary.ts
|
|
187
|
+
var m = {
|
|
188
|
+
image: "image",
|
|
189
|
+
video: "video",
|
|
190
|
+
audio: "video",
|
|
191
|
+
file: "raw"
|
|
192
|
+
};
|
|
193
|
+
function h(t) {
|
|
194
|
+
let n = (e) => {
|
|
195
|
+
var n;
|
|
196
|
+
return `https://api.cloudinary.com/v1_1/${t.cloudName}/${(n = m[e]) == null ? "raw" : n}/upload`;
|
|
197
|
+
}, r = async (r, i, a) => {
|
|
198
|
+
let o = new FormData();
|
|
199
|
+
o.append("file", i), o.append("upload_preset", t.uploadPreset), t.folder !== void 0 && o.append("folder", t.folder);
|
|
200
|
+
let { status: s, text: c } = await e({
|
|
201
|
+
method: "POST",
|
|
202
|
+
url: n(r),
|
|
203
|
+
body: o,
|
|
204
|
+
onProgress: a
|
|
205
|
+
});
|
|
206
|
+
return g(c, s, typeof i == "string" ? void 0 : i.name);
|
|
207
|
+
};
|
|
208
|
+
return {
|
|
209
|
+
uploadByFile: (e, t) => r(t.kind, e, t.onProgress),
|
|
210
|
+
uploadByUrl: (e, t) => r(t.kind, e, t.onProgress)
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function g(e, t, n) {
|
|
214
|
+
let r = _(e, t);
|
|
215
|
+
if (typeof r.secure_url != "string") {
|
|
216
|
+
var i;
|
|
217
|
+
let e = r.error;
|
|
218
|
+
throw Error(`Cloudinary upload failed: ${(i = e == null ? void 0 : e.message) == null ? `status ${t}` : i}`);
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
url: r.secure_url,
|
|
222
|
+
fileName: n
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
function _(e, t) {
|
|
226
|
+
try {
|
|
227
|
+
return JSON.parse(e);
|
|
228
|
+
} catch (e) {
|
|
229
|
+
throw Error(`Cloudinary upload failed with status ${t}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
//#endregion
|
|
233
|
+
//#region src/indexeddb.ts
|
|
234
|
+
var v = "blok-assets", y = "assets", b = "blok:asset/";
|
|
235
|
+
function x(e) {
|
|
236
|
+
return new Promise((t, n) => {
|
|
237
|
+
let r = indexedDB.open(e, 1);
|
|
238
|
+
r.onupgradeneeded = () => {
|
|
239
|
+
r.result.createObjectStore(y);
|
|
240
|
+
}, r.onsuccess = () => t(r.result), r.onerror = () => {
|
|
241
|
+
var e;
|
|
242
|
+
return n((e = r.error) == null ? /* @__PURE__ */ Error("IndexedDB open failed") : e);
|
|
243
|
+
};
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
function S(e, t, n) {
|
|
247
|
+
return new Promise((r, i) => {
|
|
248
|
+
let a = n(e.transaction(y, t).objectStore(y));
|
|
249
|
+
a.onsuccess = () => r(a.result), a.onerror = () => {
|
|
250
|
+
var e;
|
|
251
|
+
return i((e = a.error) == null ? /* @__PURE__ */ Error("IndexedDB request failed") : e);
|
|
252
|
+
};
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
function C(e = {}) {
|
|
256
|
+
var t;
|
|
257
|
+
let n = (t = e.dbName) == null ? v : t;
|
|
258
|
+
return { async uploadByFile(e, t) {
|
|
259
|
+
let r = crypto.randomUUID(), i = await x(n);
|
|
260
|
+
try {
|
|
261
|
+
await S(i, "readwrite", (t) => t.put({
|
|
262
|
+
blob: e,
|
|
263
|
+
name: e.name,
|
|
264
|
+
type: e.type
|
|
265
|
+
}, r));
|
|
266
|
+
} finally {
|
|
267
|
+
i.close();
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
url: `${b}${r}`,
|
|
271
|
+
fileName: e.name
|
|
272
|
+
};
|
|
273
|
+
} };
|
|
274
|
+
}
|
|
275
|
+
async function w(e, t = {}) {
|
|
276
|
+
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));
|
|
279
|
+
return r ? URL.createObjectURL(r.blob) : null;
|
|
280
|
+
}
|
|
281
|
+
function T(e, t) {
|
|
282
|
+
return S(e, "readonly", (e) => e.get(t)).finally(() => e.close());
|
|
283
|
+
}
|
|
284
|
+
//#endregion
|
|
285
|
+
export { h as cloudinaryStorage, s as fetchStorage, C as indexedDBStorage, p as presignedStorage, w as resolveBlokObjectUrl, d as supabaseStorage };
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bloklabs/presets",
|
|
3
|
+
"version": "1.11.0",
|
|
4
|
+
"description": "Storage presets for Blok — ready-made uploaders for Supabase, S3-compatible storage, Cloudinary, and IndexedDB.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": "JackUait",
|
|
7
|
+
"homepage": "https://blokeditor.com",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/JackUait/blok.git",
|
|
11
|
+
"directory": "packages/presets"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/JackUait/blok/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"blok",
|
|
18
|
+
"block-editor",
|
|
19
|
+
"storage",
|
|
20
|
+
"file-upload",
|
|
21
|
+
"uploader",
|
|
22
|
+
"supabase",
|
|
23
|
+
"s3",
|
|
24
|
+
"cloudinary",
|
|
25
|
+
"indexeddb",
|
|
26
|
+
"typescript"
|
|
27
|
+
],
|
|
28
|
+
"type": "module",
|
|
29
|
+
"sideEffects": false,
|
|
30
|
+
"main": "./dist/index.cjs",
|
|
31
|
+
"module": "./dist/index.mjs",
|
|
32
|
+
"types": "./types/index.d.ts",
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"types": "./types/index.d.ts",
|
|
36
|
+
"import": "./dist/index.mjs",
|
|
37
|
+
"require": "./dist/index.cjs"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"dist",
|
|
42
|
+
"types"
|
|
43
|
+
],
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "yarn run -T vite build --mode production",
|
|
49
|
+
"test": "yarn run -T vitest run"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"fake-indexeddb": "^6.2.5"
|
|
53
|
+
}
|
|
54
|
+
}
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `BlokUploader` and its supporting types below, mirrored from
|
|
3
|
+
* `types/configs/uploader.d.ts` (and `AssetKind` from
|
|
4
|
+
* `types/tools/block-tool.d.ts`) rather than imported — and deliberately not
|
|
5
|
+
* exported, since this package's public surface is only what each preset's
|
|
6
|
+
* brief asks it to produce (`fetchStorage`, `supabaseStorage`, and so on as
|
|
7
|
+
* each ships).
|
|
8
|
+
*
|
|
9
|
+
* A published `.d.ts` under `types/` ships as-is inside this package's own
|
|
10
|
+
* tarball (`files: ["dist", "types"]`) — a relative specifier like
|
|
11
|
+
* `../../../types/configs/uploader` resolves inside this monorepo, but once
|
|
12
|
+
* installed at `node_modules/@bloklabs/presets/types/index.d.ts` there is no
|
|
13
|
+
* repo root three levels up to reach, so every consumer's `tsc` would fail
|
|
14
|
+
* with TS2307. This package also declares zero dependencies, so it cannot
|
|
15
|
+
* import `@bloklabs/core`'s published types either (and `BlokUploader` is not
|
|
16
|
+
* part of that package's public surface regardless). Keep these definitions
|
|
17
|
+
* in sync with core's by hand until a generator exists (see `types/icons.d.ts`
|
|
18
|
+
* + `scripts/generate-icons-dts.mjs` for that pattern).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** The kind of asset being uploaded. */
|
|
22
|
+
type AssetKind = 'image' | 'video' | 'audio' | 'file';
|
|
23
|
+
|
|
24
|
+
/** A resolved upload result. */
|
|
25
|
+
interface UploadedAsset {
|
|
26
|
+
/** Public URL the asset is reachable at once stored. */
|
|
27
|
+
url: string;
|
|
28
|
+
/** Original filename, when the host preserved one. */
|
|
29
|
+
fileName?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Describes a single upload request.
|
|
34
|
+
*
|
|
35
|
+
* `kind` is the kind of asset being uploaded — NOT the tool that asked for
|
|
36
|
+
* it (an audio block uploading cover art sends `kind: 'image'`).
|
|
37
|
+
*/
|
|
38
|
+
interface UploadContext {
|
|
39
|
+
/** Kind of asset being uploaded. */
|
|
40
|
+
kind: AssetKind;
|
|
41
|
+
/** Name of the block tool that requested the upload, when known. */
|
|
42
|
+
tool?: string;
|
|
43
|
+
/** Upload progress in percent (0–100), when the host reports it. */
|
|
44
|
+
onProgress?(percent: number): void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Editor-level uploader, keyed by asset kind rather than by tool. Both
|
|
49
|
+
* methods are optional and resolved independently.
|
|
50
|
+
*/
|
|
51
|
+
interface BlokUploader {
|
|
52
|
+
/** Store a file chosen by the user and return its public URL. */
|
|
53
|
+
uploadByFile?(file: File, ctx: UploadContext): Promise<UploadedAsset>;
|
|
54
|
+
/** Re-host an asset the user supplied by URL, and return the stored URL. */
|
|
55
|
+
uploadByUrl?(url: string, ctx: UploadContext): Promise<UploadedAsset>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface FetchStorageOptions {
|
|
59
|
+
/** Base URL of a service speaking Blok's upload contract, e.g. `https://blok.myapp.com`. */
|
|
60
|
+
baseUrl: string;
|
|
61
|
+
/** Multipart field name the endpoint reads. Defaults to `file`. */
|
|
62
|
+
field?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Extra headers. Pass a function to mint a short-lived access pass per
|
|
65
|
+
* request — the preset never inspects what it returns.
|
|
66
|
+
*/
|
|
67
|
+
headers?: Record<string, string> | (() => Promise<Record<string, string>>);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Uploader for any endpoint speaking Blok's documented upload wire contract
|
|
72
|
+
* — including the `blok-server` sidecar, or a backend a consumer already
|
|
73
|
+
* wrote against that contract.
|
|
74
|
+
*/
|
|
75
|
+
export function fetchStorage(options: FetchStorageOptions): BlokUploader;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Structural shape of a Supabase JS client's storage API. Matched
|
|
79
|
+
* structurally rather than imported from `@supabase/supabase-js`, which this
|
|
80
|
+
* package never depends on — the consumer passes their own client instance,
|
|
81
|
+
* so their own auth session applies.
|
|
82
|
+
*/
|
|
83
|
+
export interface SupabaseLike {
|
|
84
|
+
storage: {
|
|
85
|
+
from(bucket: string): {
|
|
86
|
+
upload(
|
|
87
|
+
path: string,
|
|
88
|
+
file: File,
|
|
89
|
+
options?: { contentType?: string; upsert?: boolean }
|
|
90
|
+
): Promise<{ data: { path: string } | null; error: { message: string } | null }>;
|
|
91
|
+
getPublicUrl(path: string): { data: { publicUrl: string } };
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface SupabaseStorageOptions {
|
|
97
|
+
/** Bucket name, or a function of the asset kind for per-kind buckets. */
|
|
98
|
+
bucket?: string | ((kind: AssetKind) => string);
|
|
99
|
+
/** Object path. Defaults to a random name that keeps the original extension. */
|
|
100
|
+
path?: (file: File, ctx: UploadContext) => string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Uploader backed by a Supabase Storage bucket. Declares no `uploadByUrl`:
|
|
105
|
+
* re-hosting a remote URL needs a server-side fetch the browser cannot do, so
|
|
106
|
+
* Blok's documented fallback (store the URL verbatim) applies instead.
|
|
107
|
+
*/
|
|
108
|
+
export function supabaseStorage(client: SupabaseLike, options?: SupabaseStorageOptions): BlokUploader;
|
|
109
|
+
|
|
110
|
+
/** What the preset asks its signer for before it can PUT a file. */
|
|
111
|
+
export interface SignRequest {
|
|
112
|
+
fileName: string;
|
|
113
|
+
mimeType: string;
|
|
114
|
+
size: number;
|
|
115
|
+
kind: AssetKind;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** What the signer hands back so the browser can PUT straight to the bucket. */
|
|
119
|
+
export interface SignedTarget {
|
|
120
|
+
/** Short-lived URL the browser PUTs to. */
|
|
121
|
+
uploadUrl: string;
|
|
122
|
+
/** Where the object will be readable once stored. */
|
|
123
|
+
publicUrl: string;
|
|
124
|
+
/** Headers the signature covers — they must be sent verbatim or it fails. */
|
|
125
|
+
headers?: Record<string, string>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface PresignedStorageOptions {
|
|
129
|
+
sign(request: SignRequest): Promise<SignedTarget>;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Uploader for any provider fronted by a signer — S3, R2, or anything
|
|
134
|
+
* S3-compatible. The consumer's own backend mints the signed URL, so
|
|
135
|
+
* credentials never reach the browser. Declares no `uploadByUrl`, same
|
|
136
|
+
* reasoning as {@link supabaseStorage}.
|
|
137
|
+
*/
|
|
138
|
+
export function presignedStorage(options: PresignedStorageOptions): BlokUploader;
|
|
139
|
+
|
|
140
|
+
export interface CloudinaryStorageOptions {
|
|
141
|
+
cloudName: string;
|
|
142
|
+
/** An UNSIGNED upload preset — a signed one would need a server. */
|
|
143
|
+
uploadPreset: string;
|
|
144
|
+
folder?: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Uploader backed by Cloudinary's unsigned upload API. Unlike the other
|
|
149
|
+
* presets, this one declares a real `uploadByUrl`: Cloudinary fetches remote
|
|
150
|
+
* URLs server-side itself, so re-hosting works without any server of the
|
|
151
|
+
* consumer's own.
|
|
152
|
+
*/
|
|
153
|
+
export function cloudinaryStorage(options: CloudinaryStorageOptions): BlokUploader;
|
|
154
|
+
|
|
155
|
+
export interface IndexedDBStorageOptions {
|
|
156
|
+
dbName?: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Uploader backed by IndexedDB — for demos and local-first apps with no
|
|
161
|
+
* backend. Returns a `blok:asset/<id>` reference rather than a `blob:` URL:
|
|
162
|
+
* blob URLs are scoped to the page that created them, so saving one produces
|
|
163
|
+
* a broken image after a reload, which is exactly what this preset exists to
|
|
164
|
+
* avoid.
|
|
165
|
+
*/
|
|
166
|
+
export function indexedDBStorage(options?: IndexedDBStorageOptions): BlokUploader;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Turns a `blok:asset/<id>` reference back into a `blob:` URL an `<img>` can
|
|
170
|
+
* display. Any value that is not a `blok:asset/` reference — `http(s)`, or
|
|
171
|
+
* anything else — is returned unchanged; validating that a URL is safe to
|
|
172
|
+
* render remains the host's job. Resolves `null` when the reference is not
|
|
173
|
+
* (or no longer) stored.
|
|
174
|
+
*/
|
|
175
|
+
export function resolveBlokObjectUrl(url: string, options?: IndexedDBStorageOptions): Promise<string | null>;
|