@btst/stack 2.8.1 → 2.9.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 +3 -2
- package/dist/components/markdown/index.d.cts +15 -2
- package/dist/components/markdown/index.d.mts +15 -2
- package/dist/components/markdown/index.d.ts +15 -2
- package/dist/packages/stack/src/plugins/blog/client/components/forms/image-field.cjs +30 -1
- package/dist/packages/stack/src/plugins/blog/client/components/forms/image-field.mjs +30 -1
- package/dist/packages/stack/src/plugins/blog/client/components/forms/markdown-editor-with-overrides.cjs +49 -9
- package/dist/packages/stack/src/plugins/blog/client/components/forms/markdown-editor-with-overrides.mjs +50 -10
- package/dist/packages/stack/src/plugins/blog/client/components/forms/markdown-editor.cjs +77 -9
- package/dist/packages/stack/src/plugins/blog/client/components/forms/markdown-editor.mjs +77 -9
- package/dist/packages/stack/src/plugins/cms/client/components/forms/content-form.cjs +24 -5
- package/dist/packages/stack/src/plugins/cms/client/components/forms/content-form.mjs +24 -5
- package/dist/packages/stack/src/plugins/cms/client/components/forms/file-upload.cjs +47 -13
- package/dist/packages/stack/src/plugins/cms/client/components/forms/file-upload.mjs +47 -13
- package/dist/packages/stack/src/plugins/kanban/client/components/forms/board-form.cjs +1 -1
- package/dist/packages/stack/src/plugins/kanban/client/components/forms/board-form.mjs +1 -1
- package/dist/packages/stack/src/plugins/kanban/client/components/forms/task-form.cjs +6 -2
- package/dist/packages/stack/src/plugins/kanban/client/components/forms/task-form.mjs +6 -2
- package/dist/packages/stack/src/plugins/media/api/adapters/local.cjs +55 -0
- package/dist/packages/stack/src/plugins/media/api/adapters/local.mjs +37 -0
- package/dist/packages/stack/src/plugins/media/api/getters.cjs +83 -0
- package/dist/packages/stack/src/plugins/media/api/getters.mjs +78 -0
- package/dist/packages/stack/src/plugins/media/api/mutations.cjs +88 -0
- package/dist/packages/stack/src/plugins/media/api/mutations.mjs +82 -0
- package/dist/packages/stack/src/plugins/media/api/plugin.cjs +525 -0
- package/dist/packages/stack/src/plugins/media/api/plugin.mjs +523 -0
- package/dist/packages/stack/src/plugins/media/api/query-key-defs.cjs +19 -0
- package/dist/packages/stack/src/plugins/media/api/query-key-defs.mjs +16 -0
- package/dist/packages/stack/src/plugins/media/api/serializers.cjs +17 -0
- package/dist/packages/stack/src/plugins/media/api/serializers.mjs +14 -0
- package/dist/packages/stack/src/plugins/media/api/storage-adapter.cjs +15 -0
- package/dist/packages/stack/src/plugins/media/api/storage-adapter.mjs +11 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/asset-card.cjs +129 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/asset-card.mjs +127 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/asset-preview-button.cjs +58 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/asset-preview-button.mjs +56 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/browse-tab.cjs +94 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/browse-tab.mjs +92 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/folder-tree.cjs +171 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/folder-tree.mjs +168 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/index.cjs +308 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/index.mjs +305 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/upload-tab.cjs +104 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/upload-tab.mjs +102 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/url-tab.cjs +70 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/url-tab.mjs +68 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/utils.cjs +21 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/utils.mjs +17 -0
- package/dist/packages/stack/src/plugins/media/client/components/pages/library-page.cjs +35 -0
- package/dist/packages/stack/src/plugins/media/client/components/pages/library-page.internal.cjs +125 -0
- package/dist/packages/stack/src/plugins/media/client/components/pages/library-page.internal.mjs +123 -0
- package/dist/packages/stack/src/plugins/media/client/components/pages/library-page.mjs +33 -0
- package/dist/packages/stack/src/plugins/media/client/hooks/use-media.cjs +222 -0
- package/dist/packages/stack/src/plugins/media/client/hooks/use-media.mjs +214 -0
- package/dist/packages/stack/src/plugins/media/client/plugin.cjs +94 -0
- package/dist/packages/stack/src/plugins/media/client/plugin.mjs +92 -0
- package/dist/packages/stack/src/plugins/media/client/upload.cjs +121 -0
- package/dist/packages/stack/src/plugins/media/client/upload.mjs +119 -0
- package/dist/packages/stack/src/plugins/media/client/utils/image-compression.cjs +67 -0
- package/dist/packages/stack/src/plugins/media/client/utils/image-compression.mjs +65 -0
- package/dist/packages/stack/src/plugins/media/db.cjs +62 -0
- package/dist/packages/stack/src/plugins/media/db.mjs +60 -0
- package/dist/packages/stack/src/plugins/media/schemas.cjs +41 -0
- package/dist/packages/stack/src/plugins/media/schemas.mjs +35 -0
- package/dist/packages/ui/src/components/minimal-tiptap/components/image/image-edit-block.cjs +18 -1
- package/dist/packages/ui/src/components/minimal-tiptap/components/image/image-edit-block.mjs +19 -2
- package/dist/packages/ui/src/components/minimal-tiptap/components/image/image-edit-dialog.cjs +2 -2
- package/dist/packages/ui/src/components/minimal-tiptap/components/image/image-edit-dialog.mjs +2 -2
- package/dist/packages/ui/src/components/minimal-tiptap/components/section/five.cjs +3 -2
- package/dist/packages/ui/src/components/minimal-tiptap/components/section/five.mjs +3 -2
- package/dist/packages/ui/src/components/minimal-tiptap/minimal-tiptap.cjs +12 -5
- package/dist/packages/ui/src/components/minimal-tiptap/minimal-tiptap.mjs +12 -5
- package/dist/plugins/blog/api/index.d.cts +2 -2
- package/dist/plugins/blog/api/index.d.mts +2 -2
- package/dist/plugins/blog/api/index.d.ts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
- package/dist/plugins/blog/client/index.d.cts +60 -3
- package/dist/plugins/blog/client/index.d.mts +60 -3
- package/dist/plugins/blog/client/index.d.ts +60 -3
- package/dist/plugins/blog/query-keys.d.cts +2 -2
- package/dist/plugins/blog/query-keys.d.mts +2 -2
- package/dist/plugins/blog/query-keys.d.ts +2 -2
- package/dist/plugins/cms/client/index.d.cts +73 -3
- package/dist/plugins/cms/client/index.d.mts +73 -3
- package/dist/plugins/cms/client/index.d.ts +73 -3
- package/dist/plugins/kanban/api/index.d.cts +1 -1
- package/dist/plugins/kanban/api/index.d.mts +1 -1
- package/dist/plugins/kanban/api/index.d.ts +1 -1
- package/dist/plugins/kanban/client/hooks/index.d.cts +1 -1
- package/dist/plugins/kanban/client/hooks/index.d.mts +1 -1
- package/dist/plugins/kanban/client/hooks/index.d.ts +1 -1
- package/dist/plugins/kanban/client/index.d.cts +1 -1
- package/dist/plugins/kanban/client/index.d.mts +1 -1
- package/dist/plugins/kanban/client/index.d.ts +1 -1
- package/dist/plugins/kanban/query-keys.d.cts +1 -1
- package/dist/plugins/kanban/query-keys.d.mts +1 -1
- package/dist/plugins/kanban/query-keys.d.ts +1 -1
- package/dist/plugins/media/api/adapters/s3.cjs +106 -0
- package/dist/plugins/media/api/adapters/s3.d.cts +60 -0
- package/dist/plugins/media/api/adapters/s3.d.mts +60 -0
- package/dist/plugins/media/api/adapters/s3.d.ts +60 -0
- package/dist/plugins/media/api/adapters/s3.mjs +104 -0
- package/dist/plugins/media/api/adapters/vercel-blob.cjs +54 -0
- package/dist/plugins/media/api/adapters/vercel-blob.d.cts +41 -0
- package/dist/plugins/media/api/adapters/vercel-blob.d.mts +41 -0
- package/dist/plugins/media/api/adapters/vercel-blob.d.ts +41 -0
- package/dist/plugins/media/api/adapters/vercel-blob.mjs +52 -0
- package/dist/plugins/media/api/index.cjs +26 -0
- package/dist/plugins/media/api/index.d.cts +116 -0
- package/dist/plugins/media/api/index.d.mts +116 -0
- package/dist/plugins/media/api/index.d.ts +116 -0
- package/dist/plugins/media/api/index.mjs +6 -0
- package/dist/plugins/media/client/components/index.cjs +10 -0
- package/dist/plugins/media/client/components/index.d.cts +55 -0
- package/dist/plugins/media/client/components/index.d.mts +55 -0
- package/dist/plugins/media/client/components/index.d.ts +55 -0
- package/dist/plugins/media/client/components/index.mjs +2 -0
- package/dist/plugins/media/client/hooks/index.cjs +13 -0
- package/dist/plugins/media/client/hooks/index.d.cts +53 -0
- package/dist/plugins/media/client/hooks/index.d.mts +53 -0
- package/dist/plugins/media/client/hooks/index.d.ts +53 -0
- package/dist/plugins/media/client/hooks/index.mjs +1 -0
- package/dist/plugins/media/client/index.cjs +9 -0
- package/dist/plugins/media/client/index.d.cts +242 -0
- package/dist/plugins/media/client/index.d.mts +242 -0
- package/dist/plugins/media/client/index.d.ts +242 -0
- package/dist/plugins/media/client/index.mjs +2 -0
- package/dist/plugins/media/client.css +1 -0
- package/dist/plugins/media/query-keys.cjs +72 -0
- package/dist/plugins/media/query-keys.d.cts +49 -0
- package/dist/plugins/media/query-keys.d.mts +49 -0
- package/dist/plugins/media/query-keys.d.ts +49 -0
- package/dist/plugins/media/query-keys.mjs +70 -0
- package/dist/plugins/media/style.css +1 -0
- package/dist/shared/{stack.DOZ1EXjM.d.mts → stack.6mEHS2WH.d.mts} +3 -3
- package/dist/shared/{stack.DX-tQ93o.d.cts → stack.AJTXI7kw.d.cts} +3 -3
- package/dist/shared/{stack.DRpeDS6X.d.ts → stack.BMx2QYOK.d.ts} +25 -0
- package/dist/shared/stack.BUTXWiG-.d.ts +286 -0
- package/dist/shared/stack.C7Y9sBDg.d.mts +286 -0
- package/dist/shared/stack.C7vfOBmO.d.mts +63 -0
- package/dist/shared/stack.CAni8dnD.d.cts +63 -0
- package/dist/shared/stack.CLcnSF_b.d.cts +25 -0
- package/dist/shared/stack.CLcnSF_b.d.mts +25 -0
- package/dist/shared/stack.CLcnSF_b.d.ts +25 -0
- package/dist/shared/stack.CYSwntXC.d.ts +63 -0
- package/dist/shared/{stack.Jb0kQDJC.d.mts → stack.Cd6McBu1.d.mts} +25 -0
- package/dist/shared/stack.CoBj86jf.d.cts +109 -0
- package/dist/shared/stack.CoBj86jf.d.mts +109 -0
- package/dist/shared/stack.CoBj86jf.d.ts +109 -0
- package/dist/shared/{stack.BXxrFL9R.d.ts → stack.D7HSzZdG.d.ts} +5 -5
- package/dist/shared/{stack.DzOhpIYM.d.mts → stack.DjgpFWq3.d.cts} +5 -5
- package/dist/shared/{stack.BxFl46lB.d.cts → stack.DxQl8Wa1.d.cts} +25 -0
- package/dist/shared/{stack.BSqJrCTM.d.cts → stack.IUeyQKrm.d.mts} +5 -5
- package/dist/shared/{stack.VF6FhyZw.d.ts → stack.QYn-Px94.d.ts} +3 -3
- package/dist/shared/stack.vxskCkim.d.cts +286 -0
- package/package.json +113 -4
- package/src/plugins/blog/client/components/forms/image-field.tsx +35 -4
- package/src/plugins/blog/client/components/forms/markdown-editor-with-overrides.tsx +67 -12
- package/src/plugins/blog/client/components/forms/markdown-editor.tsx +106 -10
- package/src/plugins/blog/client/overrides.ts +58 -1
- package/src/plugins/cms/client/components/forms/content-form.tsx +26 -7
- package/src/plugins/cms/client/components/forms/file-upload.tsx +73 -15
- package/src/plugins/cms/client/overrides.ts +57 -2
- package/src/plugins/kanban/client/components/forms/board-form.tsx +1 -1
- package/src/plugins/kanban/client/components/forms/task-form.tsx +7 -1
- package/src/plugins/kanban/client/overrides.ts +25 -0
- package/src/plugins/media/__tests__/__stubs__/vercel-blob-server.ts +9 -0
- package/src/plugins/media/__tests__/getters.test.ts +274 -0
- package/src/plugins/media/__tests__/mutations.test.ts +299 -0
- package/src/plugins/media/__tests__/plugin.test.ts +752 -0
- package/src/plugins/media/__tests__/query-key-defs.test.ts +54 -0
- package/src/plugins/media/__tests__/storage-adapters.test.ts +351 -0
- package/src/plugins/media/api/adapters/local.ts +79 -0
- package/src/plugins/media/api/adapters/s3.ts +198 -0
- package/src/plugins/media/api/adapters/vercel-blob.ts +132 -0
- package/src/plugins/media/api/getters.ts +174 -0
- package/src/plugins/media/api/index.ts +41 -0
- package/src/plugins/media/api/mutations.ts +179 -0
- package/src/plugins/media/api/plugin.ts +855 -0
- package/src/plugins/media/api/query-key-defs.ts +41 -0
- package/src/plugins/media/api/serializers.ts +28 -0
- package/src/plugins/media/api/storage-adapter.ts +139 -0
- package/src/plugins/media/client/components/index.tsx +6 -0
- package/src/plugins/media/client/components/media-picker/asset-card.tsx +150 -0
- package/src/plugins/media/client/components/media-picker/asset-preview-button.tsx +67 -0
- package/src/plugins/media/client/components/media-picker/browse-tab.tsx +116 -0
- package/src/plugins/media/client/components/media-picker/folder-tree.tsx +188 -0
- package/src/plugins/media/client/components/media-picker/index.tsx +347 -0
- package/src/plugins/media/client/components/media-picker/upload-tab.tsx +108 -0
- package/src/plugins/media/client/components/media-picker/url-tab.tsx +72 -0
- package/src/plugins/media/client/components/media-picker/utils.ts +17 -0
- package/src/plugins/media/client/components/pages/library-page.internal.tsx +134 -0
- package/src/plugins/media/client/components/pages/library-page.tsx +42 -0
- package/src/plugins/media/client/hooks/index.tsx +9 -0
- package/src/plugins/media/client/hooks/use-media.tsx +289 -0
- package/src/plugins/media/client/index.ts +4 -0
- package/src/plugins/media/client/overrides.ts +127 -0
- package/src/plugins/media/client/plugin.tsx +184 -0
- package/src/plugins/media/client/upload.ts +171 -0
- package/src/plugins/media/client/utils/image-compression.ts +131 -0
- package/src/plugins/media/client.css +1 -0
- package/src/plugins/media/db.ts +62 -0
- package/src/plugins/media/query-keys.ts +96 -0
- package/src/plugins/media/schemas.ts +37 -0
- package/src/plugins/media/style.css +1 -0
- package/src/plugins/media/types.ts +26 -0
- package/dist/shared/{stack.BWp0hcm9.d.ts → stack.BQmuNl5p.d.cts} +3 -3
- package/dist/shared/{stack.BWp0hcm9.d.cts → stack.BQmuNl5p.d.mts} +3 -3
- package/dist/shared/{stack.BWp0hcm9.d.mts → stack.BQmuNl5p.d.ts} +3 -3
- package/dist/shared/{stack.BvCR4-9H.d.ts → stack.D4Cea8II.d.ts} +3 -3
- package/dist/shared/{stack.CWxAl9K3.d.mts → stack.HE_IvqV5.d.mts} +3 -3
- package/dist/shared/{stack.BOokfhZD.d.cts → stack.Rtcvl8sS.d.cts} +3 -3
|
@@ -3,7 +3,7 @@ import * as _btst_yar from '@btst/yar';
|
|
|
3
3
|
import { ComponentType } from 'react';
|
|
4
4
|
import { QueryClient } from '@tanstack/react-query';
|
|
5
5
|
import { S as SerializedBoardWithColumns } from '../../../shared/stack.DJaKVY7v.mjs';
|
|
6
|
-
export { K as KanbanPluginOverrides, a as KanbanUser } from '../../../shared/stack.
|
|
6
|
+
export { K as KanbanPluginOverrides, a as KanbanUser } from '../../../shared/stack.Cd6McBu1.mjs';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Context passed to route hooks
|
|
@@ -3,7 +3,7 @@ import * as _btst_yar from '@btst/yar';
|
|
|
3
3
|
import { ComponentType } from 'react';
|
|
4
4
|
import { QueryClient } from '@tanstack/react-query';
|
|
5
5
|
import { S as SerializedBoardWithColumns } from '../../../shared/stack.DJaKVY7v.js';
|
|
6
|
-
export { K as KanbanPluginOverrides, a as KanbanUser } from '../../../shared/stack.
|
|
6
|
+
export { K as KanbanPluginOverrides, a as KanbanUser } from '../../../shared/stack.BMx2QYOK.js';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Context passed to route hooks
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _tanstack_react_query from '@tanstack/react-query';
|
|
2
|
-
import { K as KanbanApiRouter, j as BoardsListDiscriminator } from '../../shared/stack.
|
|
2
|
+
import { K as KanbanApiRouter, j as BoardsListDiscriminator } from '../../shared/stack.Rtcvl8sS.cjs';
|
|
3
3
|
import { createApiClient } from '@btst/stack/plugins/client';
|
|
4
4
|
import { S as SerializedBoardWithColumns } from '../../shared/stack.DJaKVY7v.cjs';
|
|
5
5
|
import '@btst/stack/plugins/api';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _tanstack_react_query from '@tanstack/react-query';
|
|
2
|
-
import { K as KanbanApiRouter, j as BoardsListDiscriminator } from '../../shared/stack.
|
|
2
|
+
import { K as KanbanApiRouter, j as BoardsListDiscriminator } from '../../shared/stack.HE_IvqV5.mjs';
|
|
3
3
|
import { createApiClient } from '@btst/stack/plugins/client';
|
|
4
4
|
import { S as SerializedBoardWithColumns } from '../../shared/stack.DJaKVY7v.mjs';
|
|
5
5
|
import '@btst/stack/plugins/api';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _tanstack_react_query from '@tanstack/react-query';
|
|
2
|
-
import { K as KanbanApiRouter, j as BoardsListDiscriminator } from '../../shared/stack.
|
|
2
|
+
import { K as KanbanApiRouter, j as BoardsListDiscriminator } from '../../shared/stack.D4Cea8II.js';
|
|
3
3
|
import { createApiClient } from '@btst/stack/plugins/client';
|
|
4
4
|
import { S as SerializedBoardWithColumns } from '../../shared/stack.DJaKVY7v.js';
|
|
5
5
|
import '@btst/stack/plugins/api';
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function s3Adapter(options) {
|
|
4
|
+
const {
|
|
5
|
+
bucket,
|
|
6
|
+
region,
|
|
7
|
+
accessKeyId,
|
|
8
|
+
secretAccessKey,
|
|
9
|
+
endpoint,
|
|
10
|
+
publicBaseUrl,
|
|
11
|
+
expiresIn = 300
|
|
12
|
+
} = options;
|
|
13
|
+
let s3ModulePromise = null;
|
|
14
|
+
let clientPromise = null;
|
|
15
|
+
function getS3Module() {
|
|
16
|
+
if (!s3ModulePromise) {
|
|
17
|
+
s3ModulePromise = import('@aws-sdk/client-s3').catch(() => {
|
|
18
|
+
s3ModulePromise = null;
|
|
19
|
+
throw new Error(
|
|
20
|
+
"[@btst/stack] S3 adapter requires '@aws-sdk/client-s3'. Run: npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner"
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return s3ModulePromise;
|
|
25
|
+
}
|
|
26
|
+
function getClient() {
|
|
27
|
+
if (!clientPromise) {
|
|
28
|
+
clientPromise = getS3Module().then(({ S3Client }) => {
|
|
29
|
+
return new S3Client({
|
|
30
|
+
region,
|
|
31
|
+
endpoint,
|
|
32
|
+
credentials: { accessKeyId, secretAccessKey },
|
|
33
|
+
forcePathStyle: !!endpoint
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return clientPromise.catch((error) => {
|
|
38
|
+
if (clientPromise) {
|
|
39
|
+
clientPromise = null;
|
|
40
|
+
}
|
|
41
|
+
if (s3ModulePromise && error instanceof Error) {
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
throw new Error(
|
|
45
|
+
"[@btst/stack] S3 adapter requires '@aws-sdk/client-s3'. Run: npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner"
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
async function buildSignedUrl(client, command, opts) {
|
|
50
|
+
let getSignedUrl;
|
|
51
|
+
try {
|
|
52
|
+
({ getSignedUrl } = await import('@aws-sdk/s3-request-presigner'));
|
|
53
|
+
} catch {
|
|
54
|
+
throw new Error(
|
|
55
|
+
"[@btst/stack] S3 adapter requires '@aws-sdk/s3-request-presigner'. Run: npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner"
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return getSignedUrl(
|
|
59
|
+
client,
|
|
60
|
+
command,
|
|
61
|
+
opts
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
type: "s3",
|
|
66
|
+
urlPrefix: publicBaseUrl.replace(/\/$/, ""),
|
|
67
|
+
async generateUploadToken(uploadOptions) {
|
|
68
|
+
const [client, { PutObjectCommand }] = await Promise.all([
|
|
69
|
+
getClient(),
|
|
70
|
+
getS3Module()
|
|
71
|
+
]);
|
|
72
|
+
const key = uploadOptions.folderId ? `${uploadOptions.folderId}/${uploadOptions.filename}` : uploadOptions.filename;
|
|
73
|
+
const command = new PutObjectCommand({
|
|
74
|
+
Bucket: bucket,
|
|
75
|
+
Key: key,
|
|
76
|
+
ContentType: uploadOptions.mimeType,
|
|
77
|
+
ContentLength: uploadOptions.size
|
|
78
|
+
});
|
|
79
|
+
const uploadUrl = await buildSignedUrl(client, command, { expiresIn });
|
|
80
|
+
const encodedKey = key.split("/").map(encodeURIComponent).join("/");
|
|
81
|
+
const publicUrl = `${publicBaseUrl.replace(/\/$/, "")}/${encodedKey}`;
|
|
82
|
+
return {
|
|
83
|
+
type: "presigned-url",
|
|
84
|
+
payload: {
|
|
85
|
+
uploadUrl,
|
|
86
|
+
publicUrl,
|
|
87
|
+
key,
|
|
88
|
+
method: "PUT",
|
|
89
|
+
headers: { "Content-Type": uploadOptions.mimeType }
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
async delete(url) {
|
|
94
|
+
const [client, { DeleteObjectCommand }] = await Promise.all([
|
|
95
|
+
getClient(),
|
|
96
|
+
getS3Module()
|
|
97
|
+
]);
|
|
98
|
+
const base = publicBaseUrl.replace(/\/$/, "");
|
|
99
|
+
const encodedKey = url.startsWith(base) ? url.slice(base.length + 1) : url.split("/").pop() ?? url;
|
|
100
|
+
const key = decodeURIComponent(encodedKey);
|
|
101
|
+
await client.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
exports.s3Adapter = s3Adapter;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { S as S3StorageAdapter } from '../../../../shared/stack.CoBj86jf.cjs';
|
|
2
|
+
|
|
3
|
+
interface S3StorageAdapterOptions {
|
|
4
|
+
/**
|
|
5
|
+
* The S3 bucket name.
|
|
6
|
+
*/
|
|
7
|
+
bucket: string;
|
|
8
|
+
/**
|
|
9
|
+
* AWS region (e.g. `"us-east-1"`).
|
|
10
|
+
*/
|
|
11
|
+
region: string;
|
|
12
|
+
/**
|
|
13
|
+
* AWS access key ID.
|
|
14
|
+
*/
|
|
15
|
+
accessKeyId: string;
|
|
16
|
+
/**
|
|
17
|
+
* AWS secret access key.
|
|
18
|
+
*/
|
|
19
|
+
secretAccessKey: string;
|
|
20
|
+
/**
|
|
21
|
+
* Custom endpoint URL for S3-compatible providers (Cloudflare R2, MinIO, etc.).
|
|
22
|
+
* @example "https://<account-id>.r2.cloudflarestorage.com"
|
|
23
|
+
*/
|
|
24
|
+
endpoint?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Base URL used to construct the final public asset URL after upload.
|
|
27
|
+
* @example "https://assets.example.com" or "https://pub-<id>.r2.dev"
|
|
28
|
+
*/
|
|
29
|
+
publicBaseUrl: string;
|
|
30
|
+
/**
|
|
31
|
+
* Duration in seconds for which the presigned URL is valid.
|
|
32
|
+
* @default 300 (5 minutes)
|
|
33
|
+
*/
|
|
34
|
+
expiresIn?: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Create an S3-compatible presigned-URL storage adapter.
|
|
38
|
+
* The server generates a short-lived presigned PUT URL; the browser uploads
|
|
39
|
+
* the file directly to S3 (or R2 / MinIO). The server never receives file bytes.
|
|
40
|
+
*
|
|
41
|
+
* @remarks Requires `@aws-sdk/client-s3` and `@aws-sdk/s3-request-presigner`
|
|
42
|
+
* as optional peer dependencies.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* mediaBackendPlugin({
|
|
47
|
+
* storageAdapter: s3Adapter({
|
|
48
|
+
* bucket: "my-bucket",
|
|
49
|
+
* region: "us-east-1",
|
|
50
|
+
* accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
|
51
|
+
* secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
|
|
52
|
+
* publicBaseUrl: "https://assets.example.com",
|
|
53
|
+
* })
|
|
54
|
+
* })
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
declare function s3Adapter(options: S3StorageAdapterOptions): S3StorageAdapter;
|
|
58
|
+
|
|
59
|
+
export { s3Adapter };
|
|
60
|
+
export type { S3StorageAdapterOptions };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { S as S3StorageAdapter } from '../../../../shared/stack.CoBj86jf.mjs';
|
|
2
|
+
|
|
3
|
+
interface S3StorageAdapterOptions {
|
|
4
|
+
/**
|
|
5
|
+
* The S3 bucket name.
|
|
6
|
+
*/
|
|
7
|
+
bucket: string;
|
|
8
|
+
/**
|
|
9
|
+
* AWS region (e.g. `"us-east-1"`).
|
|
10
|
+
*/
|
|
11
|
+
region: string;
|
|
12
|
+
/**
|
|
13
|
+
* AWS access key ID.
|
|
14
|
+
*/
|
|
15
|
+
accessKeyId: string;
|
|
16
|
+
/**
|
|
17
|
+
* AWS secret access key.
|
|
18
|
+
*/
|
|
19
|
+
secretAccessKey: string;
|
|
20
|
+
/**
|
|
21
|
+
* Custom endpoint URL for S3-compatible providers (Cloudflare R2, MinIO, etc.).
|
|
22
|
+
* @example "https://<account-id>.r2.cloudflarestorage.com"
|
|
23
|
+
*/
|
|
24
|
+
endpoint?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Base URL used to construct the final public asset URL after upload.
|
|
27
|
+
* @example "https://assets.example.com" or "https://pub-<id>.r2.dev"
|
|
28
|
+
*/
|
|
29
|
+
publicBaseUrl: string;
|
|
30
|
+
/**
|
|
31
|
+
* Duration in seconds for which the presigned URL is valid.
|
|
32
|
+
* @default 300 (5 minutes)
|
|
33
|
+
*/
|
|
34
|
+
expiresIn?: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Create an S3-compatible presigned-URL storage adapter.
|
|
38
|
+
* The server generates a short-lived presigned PUT URL; the browser uploads
|
|
39
|
+
* the file directly to S3 (or R2 / MinIO). The server never receives file bytes.
|
|
40
|
+
*
|
|
41
|
+
* @remarks Requires `@aws-sdk/client-s3` and `@aws-sdk/s3-request-presigner`
|
|
42
|
+
* as optional peer dependencies.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* mediaBackendPlugin({
|
|
47
|
+
* storageAdapter: s3Adapter({
|
|
48
|
+
* bucket: "my-bucket",
|
|
49
|
+
* region: "us-east-1",
|
|
50
|
+
* accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
|
51
|
+
* secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
|
|
52
|
+
* publicBaseUrl: "https://assets.example.com",
|
|
53
|
+
* })
|
|
54
|
+
* })
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
declare function s3Adapter(options: S3StorageAdapterOptions): S3StorageAdapter;
|
|
58
|
+
|
|
59
|
+
export { s3Adapter };
|
|
60
|
+
export type { S3StorageAdapterOptions };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { S as S3StorageAdapter } from '../../../../shared/stack.CoBj86jf.js';
|
|
2
|
+
|
|
3
|
+
interface S3StorageAdapterOptions {
|
|
4
|
+
/**
|
|
5
|
+
* The S3 bucket name.
|
|
6
|
+
*/
|
|
7
|
+
bucket: string;
|
|
8
|
+
/**
|
|
9
|
+
* AWS region (e.g. `"us-east-1"`).
|
|
10
|
+
*/
|
|
11
|
+
region: string;
|
|
12
|
+
/**
|
|
13
|
+
* AWS access key ID.
|
|
14
|
+
*/
|
|
15
|
+
accessKeyId: string;
|
|
16
|
+
/**
|
|
17
|
+
* AWS secret access key.
|
|
18
|
+
*/
|
|
19
|
+
secretAccessKey: string;
|
|
20
|
+
/**
|
|
21
|
+
* Custom endpoint URL for S3-compatible providers (Cloudflare R2, MinIO, etc.).
|
|
22
|
+
* @example "https://<account-id>.r2.cloudflarestorage.com"
|
|
23
|
+
*/
|
|
24
|
+
endpoint?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Base URL used to construct the final public asset URL after upload.
|
|
27
|
+
* @example "https://assets.example.com" or "https://pub-<id>.r2.dev"
|
|
28
|
+
*/
|
|
29
|
+
publicBaseUrl: string;
|
|
30
|
+
/**
|
|
31
|
+
* Duration in seconds for which the presigned URL is valid.
|
|
32
|
+
* @default 300 (5 minutes)
|
|
33
|
+
*/
|
|
34
|
+
expiresIn?: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Create an S3-compatible presigned-URL storage adapter.
|
|
38
|
+
* The server generates a short-lived presigned PUT URL; the browser uploads
|
|
39
|
+
* the file directly to S3 (or R2 / MinIO). The server never receives file bytes.
|
|
40
|
+
*
|
|
41
|
+
* @remarks Requires `@aws-sdk/client-s3` and `@aws-sdk/s3-request-presigner`
|
|
42
|
+
* as optional peer dependencies.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* mediaBackendPlugin({
|
|
47
|
+
* storageAdapter: s3Adapter({
|
|
48
|
+
* bucket: "my-bucket",
|
|
49
|
+
* region: "us-east-1",
|
|
50
|
+
* accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
|
51
|
+
* secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
|
|
52
|
+
* publicBaseUrl: "https://assets.example.com",
|
|
53
|
+
* })
|
|
54
|
+
* })
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
declare function s3Adapter(options: S3StorageAdapterOptions): S3StorageAdapter;
|
|
58
|
+
|
|
59
|
+
export { s3Adapter };
|
|
60
|
+
export type { S3StorageAdapterOptions };
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
function s3Adapter(options) {
|
|
2
|
+
const {
|
|
3
|
+
bucket,
|
|
4
|
+
region,
|
|
5
|
+
accessKeyId,
|
|
6
|
+
secretAccessKey,
|
|
7
|
+
endpoint,
|
|
8
|
+
publicBaseUrl,
|
|
9
|
+
expiresIn = 300
|
|
10
|
+
} = options;
|
|
11
|
+
let s3ModulePromise = null;
|
|
12
|
+
let clientPromise = null;
|
|
13
|
+
function getS3Module() {
|
|
14
|
+
if (!s3ModulePromise) {
|
|
15
|
+
s3ModulePromise = import('@aws-sdk/client-s3').catch(() => {
|
|
16
|
+
s3ModulePromise = null;
|
|
17
|
+
throw new Error(
|
|
18
|
+
"[@btst/stack] S3 adapter requires '@aws-sdk/client-s3'. Run: npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner"
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return s3ModulePromise;
|
|
23
|
+
}
|
|
24
|
+
function getClient() {
|
|
25
|
+
if (!clientPromise) {
|
|
26
|
+
clientPromise = getS3Module().then(({ S3Client }) => {
|
|
27
|
+
return new S3Client({
|
|
28
|
+
region,
|
|
29
|
+
endpoint,
|
|
30
|
+
credentials: { accessKeyId, secretAccessKey },
|
|
31
|
+
forcePathStyle: !!endpoint
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return clientPromise.catch((error) => {
|
|
36
|
+
if (clientPromise) {
|
|
37
|
+
clientPromise = null;
|
|
38
|
+
}
|
|
39
|
+
if (s3ModulePromise && error instanceof Error) {
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
throw new Error(
|
|
43
|
+
"[@btst/stack] S3 adapter requires '@aws-sdk/client-s3'. Run: npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner"
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async function buildSignedUrl(client, command, opts) {
|
|
48
|
+
let getSignedUrl;
|
|
49
|
+
try {
|
|
50
|
+
({ getSignedUrl } = await import('@aws-sdk/s3-request-presigner'));
|
|
51
|
+
} catch {
|
|
52
|
+
throw new Error(
|
|
53
|
+
"[@btst/stack] S3 adapter requires '@aws-sdk/s3-request-presigner'. Run: npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner"
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
return getSignedUrl(
|
|
57
|
+
client,
|
|
58
|
+
command,
|
|
59
|
+
opts
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
type: "s3",
|
|
64
|
+
urlPrefix: publicBaseUrl.replace(/\/$/, ""),
|
|
65
|
+
async generateUploadToken(uploadOptions) {
|
|
66
|
+
const [client, { PutObjectCommand }] = await Promise.all([
|
|
67
|
+
getClient(),
|
|
68
|
+
getS3Module()
|
|
69
|
+
]);
|
|
70
|
+
const key = uploadOptions.folderId ? `${uploadOptions.folderId}/${uploadOptions.filename}` : uploadOptions.filename;
|
|
71
|
+
const command = new PutObjectCommand({
|
|
72
|
+
Bucket: bucket,
|
|
73
|
+
Key: key,
|
|
74
|
+
ContentType: uploadOptions.mimeType,
|
|
75
|
+
ContentLength: uploadOptions.size
|
|
76
|
+
});
|
|
77
|
+
const uploadUrl = await buildSignedUrl(client, command, { expiresIn });
|
|
78
|
+
const encodedKey = key.split("/").map(encodeURIComponent).join("/");
|
|
79
|
+
const publicUrl = `${publicBaseUrl.replace(/\/$/, "")}/${encodedKey}`;
|
|
80
|
+
return {
|
|
81
|
+
type: "presigned-url",
|
|
82
|
+
payload: {
|
|
83
|
+
uploadUrl,
|
|
84
|
+
publicUrl,
|
|
85
|
+
key,
|
|
86
|
+
method: "PUT",
|
|
87
|
+
headers: { "Content-Type": uploadOptions.mimeType }
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
async delete(url) {
|
|
92
|
+
const [client, { DeleteObjectCommand }] = await Promise.all([
|
|
93
|
+
getClient(),
|
|
94
|
+
getS3Module()
|
|
95
|
+
]);
|
|
96
|
+
const base = publicBaseUrl.replace(/\/$/, "");
|
|
97
|
+
const encodedKey = url.startsWith(base) ? url.slice(base.length + 1) : url.split("/").pop() ?? url;
|
|
98
|
+
const key = decodeURIComponent(encodedKey);
|
|
99
|
+
await client.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export { s3Adapter };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function vercelBlobAdapter(options = {}) {
|
|
4
|
+
return {
|
|
5
|
+
type: "vercel-blob",
|
|
6
|
+
urlHostnameSuffix: ".public.blob.vercel-storage.com",
|
|
7
|
+
async handleRequest(request, callbacks) {
|
|
8
|
+
let handleUpload;
|
|
9
|
+
try {
|
|
10
|
+
const vercelBlobServer = (
|
|
11
|
+
/* @vite-ignore */
|
|
12
|
+
// @ts-expect-error — @vercel/blob/server may not be exported in all installed versions
|
|
13
|
+
await import('@vercel/blob/server')
|
|
14
|
+
);
|
|
15
|
+
({ handleUpload } = vercelBlobServer);
|
|
16
|
+
} catch {
|
|
17
|
+
throw new Error(
|
|
18
|
+
"[@btst/stack] Vercel Blob adapter requires '@vercel/blob' with 'handleUpload' exported from '@vercel/blob/server'. Run: npm install @vercel/blob"
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
const body = await request.json();
|
|
22
|
+
return handleUpload({
|
|
23
|
+
body,
|
|
24
|
+
request,
|
|
25
|
+
token: options.token,
|
|
26
|
+
onBeforeGenerateToken: async (pathname, clientPayload) => {
|
|
27
|
+
const tokenOptions = await callbacks.onBeforeGenerateToken?.(
|
|
28
|
+
pathname,
|
|
29
|
+
clientPayload ?? null
|
|
30
|
+
) ?? {};
|
|
31
|
+
return {
|
|
32
|
+
addRandomSuffix: true,
|
|
33
|
+
...tokenOptions
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
onUploadCompleted: async () => {
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
async delete(url) {
|
|
41
|
+
let del;
|
|
42
|
+
try {
|
|
43
|
+
({ del } = await import('@vercel/blob'));
|
|
44
|
+
} catch {
|
|
45
|
+
throw new Error(
|
|
46
|
+
"[@btst/stack] Vercel Blob adapter requires '@vercel/blob'. Run: npm install @vercel/blob"
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
await del(url, options.token ? { token: options.token } : void 0);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
exports.vercelBlobAdapter = vercelBlobAdapter;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { V as VercelBlobStorageAdapter } from '../../../../shared/stack.CoBj86jf.cjs';
|
|
2
|
+
|
|
3
|
+
interface VercelBlobStorageAdapterOptions {
|
|
4
|
+
/**
|
|
5
|
+
* The `BLOB_READ_WRITE_TOKEN` environment variable is read automatically
|
|
6
|
+
* by `@vercel/blob`. You only need to provide this option if you store
|
|
7
|
+
* the token under a different name.
|
|
8
|
+
*/
|
|
9
|
+
token?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Create a Vercel Blob storage adapter using the signed direct-upload protocol.
|
|
13
|
+
* The server never receives file bytes — it only issues short-lived client tokens
|
|
14
|
+
* via `@vercel/blob`'s `handleUpload` helper (available via `@vercel/blob/server`
|
|
15
|
+
* in compatible versions).
|
|
16
|
+
*
|
|
17
|
+
* @remarks Requires `@vercel/blob` as an optional peer dependency (version
|
|
18
|
+
* with `handleUpload` exported from `@vercel/blob/server`).
|
|
19
|
+
*
|
|
20
|
+
* Upload flow:
|
|
21
|
+
* 1. Client calls `POST /media/upload/vercel-blob` to obtain a client token.
|
|
22
|
+
* 2. Client uses `@vercel/blob/client`'s `upload()` to upload directly to Vercel.
|
|
23
|
+
* 3. After upload, client calls `POST /media/assets` to save metadata to the DB.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* mediaBackendPlugin({
|
|
28
|
+
* storageAdapter: vercelBlobAdapter(),
|
|
29
|
+
* hooks: {
|
|
30
|
+
* onBeforeUpload: async (_meta, ctx) => {
|
|
31
|
+
* const session = await getSession(ctx.headers);
|
|
32
|
+
* if (!session) throw new Error("Unauthorized");
|
|
33
|
+
* },
|
|
34
|
+
* },
|
|
35
|
+
* })
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
declare function vercelBlobAdapter(options?: VercelBlobStorageAdapterOptions): VercelBlobStorageAdapter;
|
|
39
|
+
|
|
40
|
+
export { vercelBlobAdapter };
|
|
41
|
+
export type { VercelBlobStorageAdapterOptions };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { V as VercelBlobStorageAdapter } from '../../../../shared/stack.CoBj86jf.mjs';
|
|
2
|
+
|
|
3
|
+
interface VercelBlobStorageAdapterOptions {
|
|
4
|
+
/**
|
|
5
|
+
* The `BLOB_READ_WRITE_TOKEN` environment variable is read automatically
|
|
6
|
+
* by `@vercel/blob`. You only need to provide this option if you store
|
|
7
|
+
* the token under a different name.
|
|
8
|
+
*/
|
|
9
|
+
token?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Create a Vercel Blob storage adapter using the signed direct-upload protocol.
|
|
13
|
+
* The server never receives file bytes — it only issues short-lived client tokens
|
|
14
|
+
* via `@vercel/blob`'s `handleUpload` helper (available via `@vercel/blob/server`
|
|
15
|
+
* in compatible versions).
|
|
16
|
+
*
|
|
17
|
+
* @remarks Requires `@vercel/blob` as an optional peer dependency (version
|
|
18
|
+
* with `handleUpload` exported from `@vercel/blob/server`).
|
|
19
|
+
*
|
|
20
|
+
* Upload flow:
|
|
21
|
+
* 1. Client calls `POST /media/upload/vercel-blob` to obtain a client token.
|
|
22
|
+
* 2. Client uses `@vercel/blob/client`'s `upload()` to upload directly to Vercel.
|
|
23
|
+
* 3. After upload, client calls `POST /media/assets` to save metadata to the DB.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* mediaBackendPlugin({
|
|
28
|
+
* storageAdapter: vercelBlobAdapter(),
|
|
29
|
+
* hooks: {
|
|
30
|
+
* onBeforeUpload: async (_meta, ctx) => {
|
|
31
|
+
* const session = await getSession(ctx.headers);
|
|
32
|
+
* if (!session) throw new Error("Unauthorized");
|
|
33
|
+
* },
|
|
34
|
+
* },
|
|
35
|
+
* })
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
declare function vercelBlobAdapter(options?: VercelBlobStorageAdapterOptions): VercelBlobStorageAdapter;
|
|
39
|
+
|
|
40
|
+
export { vercelBlobAdapter };
|
|
41
|
+
export type { VercelBlobStorageAdapterOptions };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { V as VercelBlobStorageAdapter } from '../../../../shared/stack.CoBj86jf.js';
|
|
2
|
+
|
|
3
|
+
interface VercelBlobStorageAdapterOptions {
|
|
4
|
+
/**
|
|
5
|
+
* The `BLOB_READ_WRITE_TOKEN` environment variable is read automatically
|
|
6
|
+
* by `@vercel/blob`. You only need to provide this option if you store
|
|
7
|
+
* the token under a different name.
|
|
8
|
+
*/
|
|
9
|
+
token?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Create a Vercel Blob storage adapter using the signed direct-upload protocol.
|
|
13
|
+
* The server never receives file bytes — it only issues short-lived client tokens
|
|
14
|
+
* via `@vercel/blob`'s `handleUpload` helper (available via `@vercel/blob/server`
|
|
15
|
+
* in compatible versions).
|
|
16
|
+
*
|
|
17
|
+
* @remarks Requires `@vercel/blob` as an optional peer dependency (version
|
|
18
|
+
* with `handleUpload` exported from `@vercel/blob/server`).
|
|
19
|
+
*
|
|
20
|
+
* Upload flow:
|
|
21
|
+
* 1. Client calls `POST /media/upload/vercel-blob` to obtain a client token.
|
|
22
|
+
* 2. Client uses `@vercel/blob/client`'s `upload()` to upload directly to Vercel.
|
|
23
|
+
* 3. After upload, client calls `POST /media/assets` to save metadata to the DB.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* mediaBackendPlugin({
|
|
28
|
+
* storageAdapter: vercelBlobAdapter(),
|
|
29
|
+
* hooks: {
|
|
30
|
+
* onBeforeUpload: async (_meta, ctx) => {
|
|
31
|
+
* const session = await getSession(ctx.headers);
|
|
32
|
+
* if (!session) throw new Error("Unauthorized");
|
|
33
|
+
* },
|
|
34
|
+
* },
|
|
35
|
+
* })
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
declare function vercelBlobAdapter(options?: VercelBlobStorageAdapterOptions): VercelBlobStorageAdapter;
|
|
39
|
+
|
|
40
|
+
export { vercelBlobAdapter };
|
|
41
|
+
export type { VercelBlobStorageAdapterOptions };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
function vercelBlobAdapter(options = {}) {
|
|
2
|
+
return {
|
|
3
|
+
type: "vercel-blob",
|
|
4
|
+
urlHostnameSuffix: ".public.blob.vercel-storage.com",
|
|
5
|
+
async handleRequest(request, callbacks) {
|
|
6
|
+
let handleUpload;
|
|
7
|
+
try {
|
|
8
|
+
const vercelBlobServer = (
|
|
9
|
+
/* @vite-ignore */
|
|
10
|
+
// @ts-expect-error — @vercel/blob/server may not be exported in all installed versions
|
|
11
|
+
await import('@vercel/blob/server')
|
|
12
|
+
);
|
|
13
|
+
({ handleUpload } = vercelBlobServer);
|
|
14
|
+
} catch {
|
|
15
|
+
throw new Error(
|
|
16
|
+
"[@btst/stack] Vercel Blob adapter requires '@vercel/blob' with 'handleUpload' exported from '@vercel/blob/server'. Run: npm install @vercel/blob"
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
const body = await request.json();
|
|
20
|
+
return handleUpload({
|
|
21
|
+
body,
|
|
22
|
+
request,
|
|
23
|
+
token: options.token,
|
|
24
|
+
onBeforeGenerateToken: async (pathname, clientPayload) => {
|
|
25
|
+
const tokenOptions = await callbacks.onBeforeGenerateToken?.(
|
|
26
|
+
pathname,
|
|
27
|
+
clientPayload ?? null
|
|
28
|
+
) ?? {};
|
|
29
|
+
return {
|
|
30
|
+
addRandomSuffix: true,
|
|
31
|
+
...tokenOptions
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
onUploadCompleted: async () => {
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
async delete(url) {
|
|
39
|
+
let del;
|
|
40
|
+
try {
|
|
41
|
+
({ del } = await import('@vercel/blob'));
|
|
42
|
+
} catch {
|
|
43
|
+
throw new Error(
|
|
44
|
+
"[@btst/stack] Vercel Blob adapter requires '@vercel/blob'. Run: npm install @vercel/blob"
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
await del(url, options.token ? { token: options.token } : void 0);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export { vercelBlobAdapter };
|