@orthacms/media-provider-vercel-blob 0.4.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/LICENSE +21 -0
- package/README.md +7 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/lib/fake-blob-api.d.ts +35 -0
- package/dist/lib/fake-blob-api.d.ts.map +1 -0
- package/dist/lib/fake-blob-api.js +91 -0
- package/dist/lib/vercel-blob-storage-provider.d.ts +50 -0
- package/dist/lib/vercel-blob-storage-provider.d.ts.map +1 -0
- package/dist/lib/vercel-blob-storage-provider.js +161 -0
- package/package.json +35 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ortha CMS contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,+BAA+B,EAAE,MAAM,oCAAoC,CAAC;AACrF,YAAY,EACR,aAAa,EACb,uBAAuB,EAC1B,MAAM,oCAAoC,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createVercelBlobStorageProvider = void 0;
|
|
4
|
+
var vercel_blob_storage_provider_1 = require("./lib/vercel-blob-storage-provider");
|
|
5
|
+
Object.defineProperty(exports, "createVercelBlobStorageProvider", { enumerable: true, get: function () { return vercel_blob_storage_provider_1.createVercelBlobStorageProvider; } });
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { VercelBlobApi } from './vercel-blob-storage-provider';
|
|
2
|
+
/** One blob, as the fake holds it. */
|
|
3
|
+
interface FakeBlob {
|
|
4
|
+
body: Buffer;
|
|
5
|
+
contentType?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* A stand-in for the three `@vercel/blob` functions this adapter calls, plus a
|
|
9
|
+
* `fetch` that serves what they stored.
|
|
10
|
+
*
|
|
11
|
+
* The SDK exports free functions rather than a client, so the seam is the small
|
|
12
|
+
* `VercelBlobApi` interface the provider already accepts — the same one a
|
|
13
|
+
* caller could substitute to route through their own transport.
|
|
14
|
+
*
|
|
15
|
+
* It proves this adapter's logic and nothing about Vercel Blob. A real store is
|
|
16
|
+
* the acceptance step, and `AGENTS.md` says so.
|
|
17
|
+
*/
|
|
18
|
+
export declare class FakeBlobApi {
|
|
19
|
+
readonly blobs: Map<string, FakeBlob>;
|
|
20
|
+
/** Base the fake's public URLs hang off. */
|
|
21
|
+
readonly host = "https://fake.public.blob.vercel-storage.com";
|
|
22
|
+
/** Make the next `put` fail, to exercise the cleanup path. */
|
|
23
|
+
failNextPut: boolean;
|
|
24
|
+
/** Pathnames `del` was called for, in order. */
|
|
25
|
+
readonly deleted: string[];
|
|
26
|
+
/** Raised by `head` when set — anything that is not "missing". */
|
|
27
|
+
headError: Error | undefined;
|
|
28
|
+
readonly api: VercelBlobApi;
|
|
29
|
+
/** Serves the fake's blobs, so `get` can be exercised end to end. */
|
|
30
|
+
fetch: (input: string | URL) => Promise<Response>;
|
|
31
|
+
/** Pathnames currently held, sorted. */
|
|
32
|
+
keys(): string[];
|
|
33
|
+
}
|
|
34
|
+
export {};
|
|
35
|
+
//# sourceMappingURL=fake-blob-api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fake-blob-api.d.ts","sourceRoot":"","sources":["../../src/lib/fake-blob-api.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAEpE,sCAAsC;AACtC,UAAU,QAAQ;IACd,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAaD;;;;;;;;;;GAUG;AACH,qBAAa,WAAW;IACpB,QAAQ,CAAC,KAAK,wBAA+B;IAC7C,4CAA4C;IAC5C,QAAQ,CAAC,IAAI,iDAAiD;IAC9D,8DAA8D;IAC9D,WAAW,UAAS;IACpB,gDAAgD;IAChD,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,CAAM;IAChC,kEAAkE;IAClE,SAAS,EAAE,KAAK,GAAG,SAAS,CAAC;IAE7B,QAAQ,CAAC,GAAG,EAAE,aAAa,CA0CzB;IAEF,qEAAqE;IACrE,KAAK,GAAI,OAAO,MAAM,GAAG,GAAG,KAAG,OAAO,CAAC,QAAQ,CAAC,CAU9C;IAEF,wCAAwC;IACxC,IAAI,IAAI,MAAM,EAAE;CAGnB"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FakeBlobApi = void 0;
|
|
4
|
+
const blob_1 = require("@vercel/blob");
|
|
5
|
+
/** Drains an upload body into one buffer. */
|
|
6
|
+
async function collect(body) {
|
|
7
|
+
if (Buffer.isBuffer(body))
|
|
8
|
+
return body;
|
|
9
|
+
if (typeof body === 'string')
|
|
10
|
+
return Buffer.from(body);
|
|
11
|
+
const chunks = [];
|
|
12
|
+
for await (const chunk of body) {
|
|
13
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
14
|
+
}
|
|
15
|
+
return Buffer.concat(chunks);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A stand-in for the three `@vercel/blob` functions this adapter calls, plus a
|
|
19
|
+
* `fetch` that serves what they stored.
|
|
20
|
+
*
|
|
21
|
+
* The SDK exports free functions rather than a client, so the seam is the small
|
|
22
|
+
* `VercelBlobApi` interface the provider already accepts — the same one a
|
|
23
|
+
* caller could substitute to route through their own transport.
|
|
24
|
+
*
|
|
25
|
+
* It proves this adapter's logic and nothing about Vercel Blob. A real store is
|
|
26
|
+
* the acceptance step, and `AGENTS.md` says so.
|
|
27
|
+
*/
|
|
28
|
+
class FakeBlobApi {
|
|
29
|
+
blobs = new Map();
|
|
30
|
+
/** Base the fake's public URLs hang off. */
|
|
31
|
+
host = 'https://fake.public.blob.vercel-storage.com';
|
|
32
|
+
/** Make the next `put` fail, to exercise the cleanup path. */
|
|
33
|
+
failNextPut = false;
|
|
34
|
+
/** Pathnames `del` was called for, in order. */
|
|
35
|
+
deleted = [];
|
|
36
|
+
/** Raised by `head` when set — anything that is not "missing". */
|
|
37
|
+
headError;
|
|
38
|
+
api = {
|
|
39
|
+
put: (async (pathname, body, options) => {
|
|
40
|
+
const bytes = await collect(body);
|
|
41
|
+
if (this.failNextPut) {
|
|
42
|
+
this.failNextPut = false;
|
|
43
|
+
throw new Error('put failed');
|
|
44
|
+
}
|
|
45
|
+
this.blobs.set(pathname, {
|
|
46
|
+
body: bytes,
|
|
47
|
+
contentType: options?.contentType
|
|
48
|
+
});
|
|
49
|
+
return {
|
|
50
|
+
url: `${this.host}/${pathname}`,
|
|
51
|
+
pathname,
|
|
52
|
+
contentType: options?.contentType
|
|
53
|
+
};
|
|
54
|
+
}),
|
|
55
|
+
head: (async (pathname) => {
|
|
56
|
+
if (this.headError)
|
|
57
|
+
throw this.headError;
|
|
58
|
+
const blob = this.blobs.get(pathname);
|
|
59
|
+
if (!blob) {
|
|
60
|
+
throw new blob_1.BlobNotFoundError();
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
url: `${this.host}/${pathname}`,
|
|
64
|
+
pathname,
|
|
65
|
+
size: blob.body.byteLength,
|
|
66
|
+
contentType: blob.contentType
|
|
67
|
+
};
|
|
68
|
+
}),
|
|
69
|
+
del: (async (pathname) => {
|
|
70
|
+
for (const one of Array.isArray(pathname) ? pathname : [pathname]) {
|
|
71
|
+
this.deleted.push(one);
|
|
72
|
+
this.blobs.delete(one);
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
};
|
|
76
|
+
/** Serves the fake's blobs, so `get` can be exercised end to end. */
|
|
77
|
+
fetch = (input) => {
|
|
78
|
+
const url = typeof input === 'string' ? input : input.toString();
|
|
79
|
+
const pathname = url.slice(this.host.length + 1);
|
|
80
|
+
const blob = this.blobs.get(pathname);
|
|
81
|
+
if (!blob) {
|
|
82
|
+
return Promise.resolve(new Response(null, { status: 404 }));
|
|
83
|
+
}
|
|
84
|
+
return Promise.resolve(new Response(new Uint8Array(blob.body), { status: 200 }));
|
|
85
|
+
};
|
|
86
|
+
/** Pathnames currently held, sorted. */
|
|
87
|
+
keys() {
|
|
88
|
+
return [...this.blobs.keys()].sort();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
exports.FakeBlobApi = FakeBlobApi;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { del, head, put } from '@vercel/blob';
|
|
2
|
+
import type { StorageProvider } from '@orthacms/media-server';
|
|
3
|
+
/**
|
|
4
|
+
* The three calls this adapter makes, as an injectable seam.
|
|
5
|
+
*
|
|
6
|
+
* `@vercel/blob` exports free functions rather than a client, so there is no
|
|
7
|
+
* object to hand in — this is the shape the tests substitute, and the shape a
|
|
8
|
+
* caller could substitute to route through their own fetch stack.
|
|
9
|
+
*/
|
|
10
|
+
export interface VercelBlobApi {
|
|
11
|
+
put: typeof put;
|
|
12
|
+
head: typeof head;
|
|
13
|
+
del: typeof del;
|
|
14
|
+
}
|
|
15
|
+
/** Settings for Vercel Blob. */
|
|
16
|
+
export interface VercelBlobStorageConfig {
|
|
17
|
+
/**
|
|
18
|
+
* Read-write token. Omit on Vercel itself, where `BLOB_READ_WRITE_TOKEN` is
|
|
19
|
+
* injected into the environment and the SDK reads it.
|
|
20
|
+
*/
|
|
21
|
+
token?: string;
|
|
22
|
+
/** Prefix every pathname with this, e.g. to share a store. */
|
|
23
|
+
keyPrefix?: string;
|
|
24
|
+
/** The SDK functions, for tests or a custom transport. */
|
|
25
|
+
api?: VercelBlobApi;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Vercel Blob.
|
|
29
|
+
*
|
|
30
|
+
* **Read this before choosing it: every blob is world-readable.** Vercel Blob
|
|
31
|
+
* has one access mode, `public`, and the URL it returns is a permanent,
|
|
32
|
+
* unguessable, unauthenticated link to the bytes. Anyone who obtains that URL —
|
|
33
|
+
* from a copied `<img src>`, a browser extension, a proxy log, a forwarded
|
|
34
|
+
* email — can fetch the asset forever, with no reference to who they are.
|
|
35
|
+
*
|
|
36
|
+
* The Media Library is otherwise **private by default**: `GET
|
|
37
|
+
* /media/assets/:id/raw` checks workspace membership on every request, and a
|
|
38
|
+
* non-member gets the same 404 as a missing asset. This backend cannot uphold
|
|
39
|
+
* that for anyone holding the underlying URL. The app keeps enforcing its own
|
|
40
|
+
* rules — the API never returns the blob URL, only `/media/assets/:id/raw` —
|
|
41
|
+
* but the second copy of the bytes is public and that cannot be revoked short
|
|
42
|
+
* of deleting the blob.
|
|
43
|
+
*
|
|
44
|
+
* Fine for a marketing site's images. Not fine for a workspace whose media is
|
|
45
|
+
* confidential. That is a deployment decision, not something this adapter can
|
|
46
|
+
* paper over — so it is stated here, in `AGENTS.md`, and in the scaffolder's
|
|
47
|
+
* own hint.
|
|
48
|
+
*/
|
|
49
|
+
export declare function createVercelBlobStorageProvider(config?: VercelBlobStorageConfig): StorageProvider;
|
|
50
|
+
//# sourceMappingURL=vercel-blob-storage-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vercel-blob-storage-provider.d.ts","sourceRoot":"","sources":["../../src/lib/vercel-blob-storage-provider.ts"],"names":[],"mappings":"AAGA,OAAO,EAAqB,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAEjE,OAAO,KAAK,EAER,eAAe,EAElB,MAAM,wBAAwB,CAAC;AAEhC;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC1B,GAAG,EAAE,OAAO,GAAG,CAAC;IAChB,IAAI,EAAE,OAAO,IAAI,CAAC;IAClB,GAAG,EAAE,OAAO,GAAG,CAAC;CACnB;AAED,gCAAgC;AAChC,MAAM,WAAW,uBAAuB;IACpC;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0DAA0D;IAC1D,GAAG,CAAC,EAAE,aAAa,CAAC;CACvB;AAkBD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,+BAA+B,CAC3C,MAAM,GAAE,uBAA4B,GACrC,eAAe,CA4HjB"}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createVercelBlobStorageProvider = createVercelBlobStorageProvider;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const node_stream_1 = require("node:stream");
|
|
6
|
+
const promises_1 = require("node:stream/promises");
|
|
7
|
+
const blob_1 = require("@vercel/blob");
|
|
8
|
+
const media_server_1 = require("@orthacms/media-server");
|
|
9
|
+
/** Reduces a file name to one safe key segment. Mirrors the other providers. */
|
|
10
|
+
function sanitize(fileName) {
|
|
11
|
+
const cleaned = fileName.replace(/[^A-Za-z0-9_.-]+/g, '_');
|
|
12
|
+
return cleaned === '.' || cleaned === '..' ? `_${cleaned}` : cleaned;
|
|
13
|
+
}
|
|
14
|
+
/** True for the shapes the SDK and its CDN use to say "no such blob". */
|
|
15
|
+
function isMissing(error) {
|
|
16
|
+
const candidate = error;
|
|
17
|
+
return (error instanceof blob_1.BlobNotFoundError ||
|
|
18
|
+
candidate?.name === 'BlobNotFoundError' ||
|
|
19
|
+
candidate?.status === 404);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Vercel Blob.
|
|
23
|
+
*
|
|
24
|
+
* **Read this before choosing it: every blob is world-readable.** Vercel Blob
|
|
25
|
+
* has one access mode, `public`, and the URL it returns is a permanent,
|
|
26
|
+
* unguessable, unauthenticated link to the bytes. Anyone who obtains that URL —
|
|
27
|
+
* from a copied `<img src>`, a browser extension, a proxy log, a forwarded
|
|
28
|
+
* email — can fetch the asset forever, with no reference to who they are.
|
|
29
|
+
*
|
|
30
|
+
* The Media Library is otherwise **private by default**: `GET
|
|
31
|
+
* /media/assets/:id/raw` checks workspace membership on every request, and a
|
|
32
|
+
* non-member gets the same 404 as a missing asset. This backend cannot uphold
|
|
33
|
+
* that for anyone holding the underlying URL. The app keeps enforcing its own
|
|
34
|
+
* rules — the API never returns the blob URL, only `/media/assets/:id/raw` —
|
|
35
|
+
* but the second copy of the bytes is public and that cannot be revoked short
|
|
36
|
+
* of deleting the blob.
|
|
37
|
+
*
|
|
38
|
+
* Fine for a marketing site's images. Not fine for a workspace whose media is
|
|
39
|
+
* confidential. That is a deployment decision, not something this adapter can
|
|
40
|
+
* paper over — so it is stated here, in `AGENTS.md`, and in the scaffolder's
|
|
41
|
+
* own hint.
|
|
42
|
+
*/
|
|
43
|
+
function createVercelBlobStorageProvider(config = {}) {
|
|
44
|
+
const api = config.api ?? { put: blob_1.put, head: blob_1.head, del: blob_1.del };
|
|
45
|
+
const prefix = config.keyPrefix?.replace(/^\/+|\/+$/g, '');
|
|
46
|
+
const token = config.token ? { token: config.token } : {};
|
|
47
|
+
/** The public URL behind a stored pathname, via the metadata call. */
|
|
48
|
+
const urlFor = async (storageKey) => {
|
|
49
|
+
try {
|
|
50
|
+
const meta = await api.head(storageKey, token);
|
|
51
|
+
return meta.url;
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (isMissing(error)) {
|
|
55
|
+
throw new media_server_1.ObjectNotFoundError(storageKey, error);
|
|
56
|
+
}
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
id: 'vercel-blob',
|
|
62
|
+
capabilities: {
|
|
63
|
+
// The store's URL is public and **permanent** — it neither expires
|
|
64
|
+
// nor carries a disposition we can pin per request. The port's
|
|
65
|
+
// `directUrl` promises both, so declaring it here would be a lie
|
|
66
|
+
// that hands out an unrevocable link and serves an uploaded `.html`
|
|
67
|
+
// inline from the blob host. Downloads stay proxied, which is also
|
|
68
|
+
// what keeps the app's membership check on the request path.
|
|
69
|
+
directUrl: false,
|
|
70
|
+
contentTypeMetadata: true,
|
|
71
|
+
streamingPut: true
|
|
72
|
+
},
|
|
73
|
+
async put(object) {
|
|
74
|
+
const storageKey = [
|
|
75
|
+
prefix,
|
|
76
|
+
object.workspaceId,
|
|
77
|
+
object.assetId,
|
|
78
|
+
object.isVariant ? 'variants' : undefined,
|
|
79
|
+
sanitize(object.fileName)
|
|
80
|
+
]
|
|
81
|
+
.filter(Boolean)
|
|
82
|
+
.join('/');
|
|
83
|
+
const hash = (0, node_crypto_1.createHash)('sha256');
|
|
84
|
+
let size = 0;
|
|
85
|
+
const meter = new node_stream_1.PassThrough();
|
|
86
|
+
meter.on('data', (chunk) => {
|
|
87
|
+
hash.update(chunk);
|
|
88
|
+
size += chunk.byteLength;
|
|
89
|
+
});
|
|
90
|
+
// `pipeline` so a source that dies destroys the meter too, rather
|
|
91
|
+
// than leaving the upload waiting on a stream that never ends.
|
|
92
|
+
//
|
|
93
|
+
// Awaited **together** with the upload, not after it: whichever
|
|
94
|
+
// fails first, both promises already carry a handler. Awaiting them
|
|
95
|
+
// in sequence meant that when the upload rejected first, the
|
|
96
|
+
// pipeline's own rejection had nobody attached yet — an unhandled
|
|
97
|
+
// rejection, which in Node is a killed process rather than a failed
|
|
98
|
+
// request.
|
|
99
|
+
try {
|
|
100
|
+
await Promise.all([
|
|
101
|
+
api.put(storageKey, meter, {
|
|
102
|
+
access: 'public',
|
|
103
|
+
contentType: object.contentType,
|
|
104
|
+
// The pathname we computed **is** the storage key. With
|
|
105
|
+
// the default suffix on, the stored blob would live
|
|
106
|
+
// somewhere this adapter cannot name, and `get` /
|
|
107
|
+
// `remove` would miss every object they were handed.
|
|
108
|
+
addRandomSuffix: false,
|
|
109
|
+
...token
|
|
110
|
+
}),
|
|
111
|
+
(0, promises_1.pipeline)(object.body, meter)
|
|
112
|
+
]);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
await api.del(storageKey, token).catch(() => undefined);
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
return { storageKey, size, checksum: hash.digest('hex') };
|
|
119
|
+
},
|
|
120
|
+
async get(storageKey) {
|
|
121
|
+
// Two steps, and the first is the important one: `head` turns a
|
|
122
|
+
// missing blob into a rejection *before* any stream exists, which
|
|
123
|
+
// is what the port promises and what lets the route answer 404
|
|
124
|
+
// rather than a streaming 200 it cannot take back.
|
|
125
|
+
const url = await urlFor(storageKey);
|
|
126
|
+
const response = await fetch(url);
|
|
127
|
+
if (!response.ok || !response.body) {
|
|
128
|
+
if (response.status === 404) {
|
|
129
|
+
throw new media_server_1.ObjectNotFoundError(storageKey);
|
|
130
|
+
}
|
|
131
|
+
throw new Error(`Vercel Blob refused to serve ${storageKey}: ${response.status} ${response.statusText}`);
|
|
132
|
+
}
|
|
133
|
+
return node_stream_1.Readable.fromWeb(response.body);
|
|
134
|
+
},
|
|
135
|
+
async remove(storageKey) {
|
|
136
|
+
try {
|
|
137
|
+
await api.del(storageKey, token);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
// Reclaim is post-commit and best-effort, so a blob that is
|
|
141
|
+
// already gone is a success, not something to report.
|
|
142
|
+
if (!isMissing(error))
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
async verify() {
|
|
147
|
+
// There is no store-level metadata call, so this asks about a
|
|
148
|
+
// pathname that will not exist: a missing blob proves the token and
|
|
149
|
+
// the store are good, while a bad token raises something else and
|
|
150
|
+
// fails the boot.
|
|
151
|
+
try {
|
|
152
|
+
await api.head('__ortha_verify__/does-not-exist', token);
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
if (isMissing(error))
|
|
156
|
+
return;
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@orthacms/media-provider-vercel-blob",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "@orthacms/media-provider-vercel-blob — part of Ortha CMS.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://github.com/ortha-source/ortha-cms/tree/main/packages/media/provider-vercel-blob",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/ortha-source/ortha-cms.git",
|
|
10
|
+
"directory": "packages/media/provider-vercel-blob"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/ortha-source/ortha-cms/issues"
|
|
14
|
+
},
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"default": "./dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./package.json": "./package.json"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@orthacms/media-server": "^0.4.0",
|
|
29
|
+
"@vercel/blob": "^0.27.0",
|
|
30
|
+
"tslib": "^2.3.0"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
}
|
|
35
|
+
}
|