@emdash-cms/cloudflare 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/index.d.mts +81 -0
- package/dist/auth/index.mjs +147 -0
- package/dist/cache/config.d.mts +52 -0
- package/dist/cache/config.mjs +55 -0
- package/dist/cache/runtime.d.mts +40 -0
- package/dist/cache/runtime.mjs +191 -0
- package/dist/d1-introspector-bZf0_ylK.mjs +57 -0
- package/dist/db/d1.d.mts +43 -0
- package/dist/db/d1.mjs +74 -0
- package/dist/db/do.d.mts +96 -0
- package/dist/db/do.mjs +489 -0
- package/dist/db/playground-middleware.d.mts +20 -0
- package/dist/db/playground-middleware.mjs +533 -0
- package/dist/db/playground.d.mts +39 -0
- package/dist/db/playground.mjs +26 -0
- package/dist/do-class-DY2Ba2RJ.mjs +174 -0
- package/dist/do-class-x5Xh_G62.d.mts +73 -0
- package/dist/do-dialect-BhFcRSFQ.mjs +58 -0
- package/dist/do-playground-routes-CmwFeGwJ.mjs +49 -0
- package/dist/do-types-CY0G0oyh.d.mts +14 -0
- package/dist/images-4RT9Ag8_.d.mts +76 -0
- package/dist/index.d.mts +200 -0
- package/dist/index.mjs +214 -0
- package/dist/media/images-runtime.d.mts +10 -0
- package/dist/media/images-runtime.mjs +215 -0
- package/dist/media/stream-runtime.d.mts +10 -0
- package/dist/media/stream-runtime.mjs +218 -0
- package/dist/plugins/index.d.mts +32 -0
- package/dist/plugins/index.mjs +163 -0
- package/dist/sandbox/index.d.mts +255 -0
- package/dist/sandbox/index.mjs +945 -0
- package/dist/storage/r2.d.mts +31 -0
- package/dist/storage/r2.mjs +116 -0
- package/dist/stream-DdbcvKi0.d.mts +78 -0
- package/package.json +109 -0
- package/src/auth/cloudflare-access.ts +303 -0
- package/src/auth/index.ts +16 -0
- package/src/cache/config.ts +81 -0
- package/src/cache/runtime.ts +328 -0
- package/src/cloudflare.d.ts +31 -0
- package/src/db/d1-introspector.ts +120 -0
- package/src/db/d1.ts +112 -0
- package/src/db/do-class.ts +275 -0
- package/src/db/do-dialect.ts +125 -0
- package/src/db/do-playground-routes.ts +65 -0
- package/src/db/do-preview-routes.ts +48 -0
- package/src/db/do-preview-sign.ts +100 -0
- package/src/db/do-preview.ts +268 -0
- package/src/db/do-types.ts +12 -0
- package/src/db/do.ts +62 -0
- package/src/db/playground-middleware.ts +340 -0
- package/src/db/playground-toolbar.ts +341 -0
- package/src/db/playground.ts +49 -0
- package/src/db/preview-toolbar.ts +220 -0
- package/src/index.ts +285 -0
- package/src/media/images-runtime.ts +353 -0
- package/src/media/images.ts +114 -0
- package/src/media/stream-runtime.ts +392 -0
- package/src/media/stream.ts +118 -0
- package/src/plugins/index.ts +7 -0
- package/src/plugins/vectorize-search.ts +393 -0
- package/src/sandbox/bridge.ts +1008 -0
- package/src/sandbox/index.ts +13 -0
- package/src/sandbox/runner.ts +357 -0
- package/src/sandbox/types.ts +181 -0
- package/src/sandbox/wrapper.ts +238 -0
- package/src/storage/r2.ts +200 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { cloudflareCache } from "./cache/config.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/media/images.ts
|
|
4
|
+
const IMAGES_ICON = `data:image/svg+xml,${encodeURIComponent("<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"64\" height=\"64\" fill=\"none\" viewBox=\"0 0 64 64\"><path fill=\"#F63\" d=\"M56 11.92H8l-2 2v39.87l2 2h48l2-2V13.92l-2-2Zm-2 4v18.69l-8-6.55-2.62.08-5.08 4.68-5.43-4-2.47.08-14 11.7-6.4-4.4V15.92h44ZM10 51.79V41.08l5.3 3.7 2.42-.11L31.75 33l5.5 4 2.54-.14 5-4.63L54 39.77v12l-44 .02Z\"/><path fill=\"#F63\" d=\"M19.08 32.16a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z\"/></svg>")}`;
|
|
5
|
+
/**
|
|
6
|
+
* Cloudflare Images media provider
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { cloudflareImages } from "@emdash-cms/cloudflare";
|
|
11
|
+
*
|
|
12
|
+
* emdash({
|
|
13
|
+
* mediaProviders: [
|
|
14
|
+
* // Uses CF_ACCOUNT_ID and CF_IMAGES_TOKEN env vars by default
|
|
15
|
+
* cloudflareImages({}),
|
|
16
|
+
*
|
|
17
|
+
* // Or with custom env var names
|
|
18
|
+
* cloudflareImages({
|
|
19
|
+
* accountIdEnvVar: "MY_CF_ACCOUNT",
|
|
20
|
+
* apiTokenEnvVar: "MY_CF_IMAGES_KEY",
|
|
21
|
+
* }),
|
|
22
|
+
* ],
|
|
23
|
+
* })
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
function cloudflareImages(config) {
|
|
27
|
+
return {
|
|
28
|
+
id: "cloudflare-images",
|
|
29
|
+
name: "Cloudflare Images",
|
|
30
|
+
icon: IMAGES_ICON,
|
|
31
|
+
entrypoint: "@emdash-cms/cloudflare/media/images-runtime",
|
|
32
|
+
capabilities: {
|
|
33
|
+
browse: true,
|
|
34
|
+
search: false,
|
|
35
|
+
upload: true,
|
|
36
|
+
delete: true
|
|
37
|
+
},
|
|
38
|
+
config
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/media/stream.ts
|
|
44
|
+
const STREAM_ICON = `data:image/svg+xml,${encodeURIComponent("<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"64\" height=\"64\" fill=\"none\" viewBox=\"0 0 64 64\"><g clip-path=\"url(#a)\"><path fill=\"#F63\" d=\"M59.87 30.176a11.73 11.73 0 0 0-8-2.72 19.3 19.3 0 0 0-37-4.59 13.63 13.63 0 0 0-9.67 3.19 14.599 14.599 0 0 0-5.2 11 14.24 14.24 0 0 0 14.18 14.25h37.88a12 12 0 0 0 7.81-21.13Zm-7.81 17.13H14.19A10.24 10.24 0 0 1 4 37.086a10.58 10.58 0 0 1 3.77-8 9.55 9.55 0 0 1 6.23-2.25c.637 0 1.273.058 1.9.17l1.74.31.51-1.69A15.29 15.29 0 0 1 48 29.686l.1 2.32 2.26-.36a8.239 8.239 0 0 1 6.91 1.62 8.098 8.098 0 0 1 2.73 6.1 8 8 0 0 1-7.94 7.94Z\"/><path fill=\"#F63\" fill-rule=\"evenodd\" d=\"m25.72 24.89 3.02-1.72 15.085 8.936.004 3.44-15.087 8.973L25.72 42.8V24.89Zm4 3.51v10.883l9.168-5.452L29.72 28.4Z\" clip-rule=\"evenodd\"/></g><defs><clipPath id=\"a\"><path fill=\"#fff\" d=\"M0 0h64v64H0z\"/></clipPath></defs></svg>")}`;
|
|
45
|
+
/**
|
|
46
|
+
* Cloudflare Stream media provider
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```ts
|
|
50
|
+
* import { cloudflareStream } from "@emdash-cms/cloudflare";
|
|
51
|
+
*
|
|
52
|
+
* emdash({
|
|
53
|
+
* mediaProviders: [
|
|
54
|
+
* // Uses CF_ACCOUNT_ID and CF_STREAM_TOKEN env vars by default
|
|
55
|
+
* cloudflareStream({}),
|
|
56
|
+
*
|
|
57
|
+
* // Or with custom env var names
|
|
58
|
+
* cloudflareStream({
|
|
59
|
+
* accountIdEnvVar: "MY_CF_ACCOUNT",
|
|
60
|
+
* apiTokenEnvVar: "MY_CF_STREAM_KEY",
|
|
61
|
+
* }),
|
|
62
|
+
* ],
|
|
63
|
+
* })
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
function cloudflareStream(config) {
|
|
67
|
+
return {
|
|
68
|
+
id: "cloudflare-stream",
|
|
69
|
+
name: "Cloudflare Stream",
|
|
70
|
+
icon: STREAM_ICON,
|
|
71
|
+
entrypoint: "@emdash-cms/cloudflare/media/stream-runtime",
|
|
72
|
+
capabilities: {
|
|
73
|
+
browse: true,
|
|
74
|
+
search: true,
|
|
75
|
+
upload: true,
|
|
76
|
+
delete: true
|
|
77
|
+
},
|
|
78
|
+
config
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/index.ts
|
|
84
|
+
/**
|
|
85
|
+
* Cloudflare D1 database adapter
|
|
86
|
+
*
|
|
87
|
+
* For Cloudflare Workers with D1 binding.
|
|
88
|
+
* Migrations run automatically at setup time - no need for manual SQL files.
|
|
89
|
+
*
|
|
90
|
+
* Uses a custom introspector that works around D1's restriction on
|
|
91
|
+
* cross-joins with pragma_table_info().
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```ts
|
|
95
|
+
* database: d1({ binding: "DB" })
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
function d1(config) {
|
|
99
|
+
return {
|
|
100
|
+
entrypoint: "@emdash-cms/cloudflare/db/d1",
|
|
101
|
+
config,
|
|
102
|
+
type: "sqlite"
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Durable Object preview database adapter
|
|
107
|
+
*
|
|
108
|
+
* Each preview session gets an isolated SQLite database inside a DO,
|
|
109
|
+
* populated from a snapshot of the source EmDash site.
|
|
110
|
+
*
|
|
111
|
+
* Not for production use — preview only.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```ts
|
|
115
|
+
* database: previewDatabase({ binding: "PREVIEW_DB" })
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
function previewDatabase(config) {
|
|
119
|
+
return {
|
|
120
|
+
entrypoint: "@emdash-cms/cloudflare/db/do",
|
|
121
|
+
config,
|
|
122
|
+
type: "sqlite"
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Durable Object playground database adapter
|
|
127
|
+
*
|
|
128
|
+
* Each playground session gets an isolated SQLite database inside a DO,
|
|
129
|
+
* populated from a seed file with migrations run at init time.
|
|
130
|
+
* Unlike preview, playground is writable and has admin access.
|
|
131
|
+
*
|
|
132
|
+
* Not for production use -- playground/demo only.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```ts
|
|
136
|
+
* database: playgroundDatabase({ binding: "PLAYGROUND_DB" })
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
function playgroundDatabase(config) {
|
|
140
|
+
return {
|
|
141
|
+
entrypoint: "@emdash-cms/cloudflare/db/playground",
|
|
142
|
+
config,
|
|
143
|
+
type: "sqlite"
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Cloudflare R2 binding adapter
|
|
148
|
+
*
|
|
149
|
+
* Uses R2 bindings directly when running on Cloudflare Workers.
|
|
150
|
+
* Does NOT support signed upload URLs (use s3() with R2 credentials instead).
|
|
151
|
+
*
|
|
152
|
+
* Requires R2 binding in wrangler.toml:
|
|
153
|
+
* ```toml
|
|
154
|
+
* [[r2_buckets]]
|
|
155
|
+
* binding = "MEDIA"
|
|
156
|
+
* bucket_name = "my-media-bucket"
|
|
157
|
+
* ```
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* ```ts
|
|
161
|
+
* storage: r2({ binding: "MEDIA" })
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
function r2(config) {
|
|
165
|
+
return {
|
|
166
|
+
entrypoint: "@emdash-cms/cloudflare/storage/r2",
|
|
167
|
+
config: {
|
|
168
|
+
binding: config.binding,
|
|
169
|
+
publicUrl: config.publicUrl
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Cloudflare Access authentication adapter
|
|
175
|
+
*
|
|
176
|
+
* Use this to configure EmDash to authenticate via Cloudflare Access.
|
|
177
|
+
* When Access is configured, passkey auth is disabled.
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* ```ts
|
|
181
|
+
* auth: access({
|
|
182
|
+
* teamDomain: "myteam.cloudflareaccess.com",
|
|
183
|
+
* audience: "abc123...",
|
|
184
|
+
* roleMapping: {
|
|
185
|
+
* "Admins": 50,
|
|
186
|
+
* "Editors": 30,
|
|
187
|
+
* },
|
|
188
|
+
* })
|
|
189
|
+
* ```
|
|
190
|
+
*/
|
|
191
|
+
function access(config) {
|
|
192
|
+
return {
|
|
193
|
+
type: "cloudflare-access",
|
|
194
|
+
entrypoint: "@emdash-cms/cloudflare/auth",
|
|
195
|
+
config
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Cloudflare Worker Loader sandbox adapter
|
|
200
|
+
*
|
|
201
|
+
* Returns the module path for the Cloudflare sandbox runner.
|
|
202
|
+
* Use this in the `sandboxRunner` config option.
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* ```ts
|
|
206
|
+
* sandboxRunner: sandbox()
|
|
207
|
+
* ```
|
|
208
|
+
*/
|
|
209
|
+
function sandbox() {
|
|
210
|
+
return "@emdash-cms/cloudflare/sandbox";
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
//#endregion
|
|
214
|
+
export { access, cloudflareCache, cloudflareImages, cloudflareStream, d1, playgroundDatabase, previewDatabase, r2, sandbox };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { t as CloudflareImagesConfig } from "../images-4RT9Ag8_.mjs";
|
|
2
|
+
import { CreateMediaProviderFn } from "emdash/media";
|
|
3
|
+
|
|
4
|
+
//#region src/media/images-runtime.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Runtime implementation for Cloudflare Images provider
|
|
7
|
+
*/
|
|
8
|
+
declare const createMediaProvider: CreateMediaProviderFn<CloudflareImagesConfig>;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { createMediaProvider };
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { env } from "cloudflare:workers";
|
|
2
|
+
|
|
3
|
+
//#region src/media/images-runtime.ts
|
|
4
|
+
/**
|
|
5
|
+
* Cloudflare Images Runtime Module
|
|
6
|
+
*
|
|
7
|
+
* This module is imported at runtime by the media provider system.
|
|
8
|
+
* It contains the actual provider implementation that interacts with the Cloudflare API.
|
|
9
|
+
*/
|
|
10
|
+
/** Safely extract a number from an unknown value */
|
|
11
|
+
function toNumber(value) {
|
|
12
|
+
return typeof value === "number" ? value : void 0;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Resolve a config value, checking env var if direct value not provided
|
|
16
|
+
*/
|
|
17
|
+
function resolveEnvValue(directValue, envVarName, defaultEnvVar, serviceName) {
|
|
18
|
+
if (directValue) return directValue;
|
|
19
|
+
const envVar = envVarName || defaultEnvVar;
|
|
20
|
+
const value = env[envVar];
|
|
21
|
+
if (!value) throw new Error(`${serviceName}: Missing ${envVar}. Set it as an environment variable or provide it directly in config.`);
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Runtime implementation for Cloudflare Images provider
|
|
26
|
+
*/
|
|
27
|
+
const createMediaProvider = (config) => {
|
|
28
|
+
const { deliveryDomain, defaultVariant = "public" } = config;
|
|
29
|
+
const getAccountId = () => resolveEnvValue(config.accountId, config.accountIdEnvVar, "CF_ACCOUNT_ID", "Cloudflare Images");
|
|
30
|
+
const getAccountHash = () => resolveEnvValue(config.accountHash, config.accountHashEnvVar, "CF_IMAGES_ACCOUNT_HASH", "Cloudflare Images");
|
|
31
|
+
const getApiToken = () => resolveEnvValue(config.apiToken, config.apiTokenEnvVar, "CF_IMAGES_TOKEN", "Cloudflare Images");
|
|
32
|
+
const getApiBase = () => `https://api.cloudflare.com/client/v4/accounts/${getAccountId()}/images/v1`;
|
|
33
|
+
const getHeaders = () => ({ Authorization: `Bearer ${getApiToken()}` });
|
|
34
|
+
const getDeliveryBase = () => deliveryDomain ? `https://${deliveryDomain}` : "https://imagedelivery.net";
|
|
35
|
+
const buildUrl = (imageId, transforms) => {
|
|
36
|
+
const base = `${getDeliveryBase()}/${getAccountHash()}/${imageId}`;
|
|
37
|
+
if (!transforms || Object.keys(transforms).length === 0) return `${base}/${defaultVariant}`;
|
|
38
|
+
const parts = [];
|
|
39
|
+
if (transforms.w) parts.push(`w=${transforms.w}`);
|
|
40
|
+
if (transforms.h) parts.push(`h=${transforms.h}`);
|
|
41
|
+
if (transforms.fit) parts.push(`fit=${transforms.fit}`);
|
|
42
|
+
return `${base}/${parts.join(",")}`;
|
|
43
|
+
};
|
|
44
|
+
const fetchDimensions = async (imageId) => {
|
|
45
|
+
const url = `${getDeliveryBase()}/${getAccountHash()}/${imageId}/format=json`;
|
|
46
|
+
try {
|
|
47
|
+
const response = await fetch(url);
|
|
48
|
+
if (!response.ok) return null;
|
|
49
|
+
const data = await response.json();
|
|
50
|
+
return {
|
|
51
|
+
width: data.width,
|
|
52
|
+
height: data.height
|
|
53
|
+
};
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
async list(options) {
|
|
60
|
+
const apiBase = getApiBase();
|
|
61
|
+
const headers = getHeaders();
|
|
62
|
+
const params = new URLSearchParams();
|
|
63
|
+
if (options.cursor) params.set("continuation_token", options.cursor);
|
|
64
|
+
if (options.limit) params.set("per_page", String(options.limit));
|
|
65
|
+
const url = `${apiBase}?${params}`;
|
|
66
|
+
const response = await fetch(url, { headers });
|
|
67
|
+
if (!response.ok) throw new Error(`Cloudflare Images API error: ${response.status}`);
|
|
68
|
+
const data = await response.json();
|
|
69
|
+
if (!data.success) throw new Error(`Cloudflare Images API error: ${data.errors?.[0]?.message || "Unknown error"}`);
|
|
70
|
+
const publicImages = data.result.images.filter((img) => !img.requireSignedURLs);
|
|
71
|
+
const dimensionsMap = /* @__PURE__ */ new Map();
|
|
72
|
+
const dimensionResults = await Promise.all(publicImages.map(async (img) => {
|
|
73
|
+
const dims = await fetchDimensions(img.id);
|
|
74
|
+
return {
|
|
75
|
+
id: img.id,
|
|
76
|
+
dims
|
|
77
|
+
};
|
|
78
|
+
}));
|
|
79
|
+
for (const { id, dims } of dimensionResults) if (dims) dimensionsMap.set(id, dims);
|
|
80
|
+
return {
|
|
81
|
+
items: publicImages.map((img) => {
|
|
82
|
+
const dims = dimensionsMap.get(img.id);
|
|
83
|
+
return {
|
|
84
|
+
id: img.id,
|
|
85
|
+
filename: img.filename || img.id,
|
|
86
|
+
mimeType: "image/jpeg",
|
|
87
|
+
width: dims?.width ?? toNumber(img.meta?.width),
|
|
88
|
+
height: dims?.height ?? toNumber(img.meta?.height),
|
|
89
|
+
previewUrl: buildUrl(img.id, {
|
|
90
|
+
w: 400,
|
|
91
|
+
fit: "scale-down"
|
|
92
|
+
}),
|
|
93
|
+
meta: {
|
|
94
|
+
variants: img.variants,
|
|
95
|
+
uploaded: img.uploaded
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}),
|
|
99
|
+
nextCursor: data.result.continuation_token || void 0
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
async get(id) {
|
|
103
|
+
const apiBase = getApiBase();
|
|
104
|
+
const headers = getHeaders();
|
|
105
|
+
const url = `${apiBase}/${id}`;
|
|
106
|
+
const response = await fetch(url, { headers });
|
|
107
|
+
if (!response.ok) {
|
|
108
|
+
if (response.status === 404) return null;
|
|
109
|
+
throw new Error(`Cloudflare Images API error: ${response.status}`);
|
|
110
|
+
}
|
|
111
|
+
const data = await response.json();
|
|
112
|
+
if (!data.success) return null;
|
|
113
|
+
const img = data.result;
|
|
114
|
+
if (img.requireSignedURLs) return null;
|
|
115
|
+
const dims = await fetchDimensions(img.id);
|
|
116
|
+
return {
|
|
117
|
+
id: img.id,
|
|
118
|
+
filename: img.filename || img.id,
|
|
119
|
+
mimeType: "image/jpeg",
|
|
120
|
+
width: dims?.width ?? toNumber(img.meta?.width),
|
|
121
|
+
height: dims?.height ?? toNumber(img.meta?.height),
|
|
122
|
+
previewUrl: buildUrl(img.id, {
|
|
123
|
+
w: 800,
|
|
124
|
+
fit: "scale-down"
|
|
125
|
+
}),
|
|
126
|
+
meta: {
|
|
127
|
+
variants: img.variants,
|
|
128
|
+
uploaded: img.uploaded
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
},
|
|
132
|
+
async upload(input) {
|
|
133
|
+
const apiBase = getApiBase();
|
|
134
|
+
const apiToken = getApiToken();
|
|
135
|
+
const formData = new FormData();
|
|
136
|
+
formData.append("file", input.file, input.filename);
|
|
137
|
+
formData.append("requireSignedURLs", "false");
|
|
138
|
+
const metadata = {};
|
|
139
|
+
if (input.alt) metadata.alt = input.alt;
|
|
140
|
+
if (Object.keys(metadata).length > 0) formData.append("metadata", JSON.stringify(metadata));
|
|
141
|
+
const response = await fetch(apiBase, {
|
|
142
|
+
method: "POST",
|
|
143
|
+
headers: { Authorization: `Bearer ${apiToken}` },
|
|
144
|
+
body: formData
|
|
145
|
+
});
|
|
146
|
+
if (!response.ok) {
|
|
147
|
+
const error = await response.text();
|
|
148
|
+
throw new Error(`Cloudflare Images upload failed: ${error}`);
|
|
149
|
+
}
|
|
150
|
+
const data = await response.json();
|
|
151
|
+
if (!data.success) throw new Error(`Cloudflare Images upload failed: ${data.errors?.[0]?.message || "Unknown error"}`);
|
|
152
|
+
const img = data.result;
|
|
153
|
+
return {
|
|
154
|
+
id: img.id,
|
|
155
|
+
filename: img.filename || input.filename,
|
|
156
|
+
mimeType: "image/jpeg",
|
|
157
|
+
width: toNumber(img.meta?.width),
|
|
158
|
+
height: toNumber(img.meta?.height),
|
|
159
|
+
previewUrl: buildUrl(img.id, {
|
|
160
|
+
w: 400,
|
|
161
|
+
fit: "scale-down"
|
|
162
|
+
}),
|
|
163
|
+
meta: {
|
|
164
|
+
variants: img.variants,
|
|
165
|
+
uploaded: img.uploaded
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
},
|
|
169
|
+
async delete(id) {
|
|
170
|
+
const apiBase = getApiBase();
|
|
171
|
+
const headers = getHeaders();
|
|
172
|
+
const response = await fetch(`${apiBase}/${id}`, {
|
|
173
|
+
method: "DELETE",
|
|
174
|
+
headers
|
|
175
|
+
});
|
|
176
|
+
if (!response.ok && response.status !== 404) throw new Error(`Cloudflare Images delete failed: ${response.status}`);
|
|
177
|
+
},
|
|
178
|
+
getEmbed(value, options) {
|
|
179
|
+
const accountHash = getAccountHash();
|
|
180
|
+
const baseUrl = `${getDeliveryBase()}/${accountHash}/${value.id}`;
|
|
181
|
+
const buildSrc = (opts) => {
|
|
182
|
+
const t = [];
|
|
183
|
+
if (opts.width) t.push(`w=${opts.width}`);
|
|
184
|
+
if (opts.height) t.push(`h=${opts.height}`);
|
|
185
|
+
if (opts.format) t.push(`f=${opts.format}`);
|
|
186
|
+
t.push("fit=scale-down");
|
|
187
|
+
return `${baseUrl}/${t.join(",")}`;
|
|
188
|
+
};
|
|
189
|
+
return {
|
|
190
|
+
type: "image",
|
|
191
|
+
src: buildSrc({
|
|
192
|
+
width: options?.width ?? value.width ?? 1200,
|
|
193
|
+
height: options?.height ?? value.height,
|
|
194
|
+
format: options?.format
|
|
195
|
+
}),
|
|
196
|
+
width: options?.width ?? value.width,
|
|
197
|
+
height: options?.height ?? value.height,
|
|
198
|
+
alt: value.alt,
|
|
199
|
+
getSrc: buildSrc
|
|
200
|
+
};
|
|
201
|
+
},
|
|
202
|
+
getThumbnailUrl(id, _mimeType, options) {
|
|
203
|
+
const width = options?.width || 400;
|
|
204
|
+
const height = options?.height;
|
|
205
|
+
return buildUrl(id, {
|
|
206
|
+
w: width,
|
|
207
|
+
h: height,
|
|
208
|
+
fit: "scale-down"
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
//#endregion
|
|
215
|
+
export { createMediaProvider };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { t as CloudflareStreamConfig } from "../stream-DdbcvKi0.mjs";
|
|
2
|
+
import { CreateMediaProviderFn } from "emdash/media";
|
|
3
|
+
|
|
4
|
+
//#region src/media/stream-runtime.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Runtime implementation for Cloudflare Stream provider
|
|
7
|
+
*/
|
|
8
|
+
declare const createMediaProvider: CreateMediaProviderFn<CloudflareStreamConfig>;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { createMediaProvider };
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { env } from "cloudflare:workers";
|
|
2
|
+
|
|
3
|
+
//#region src/media/stream-runtime.ts
|
|
4
|
+
/**
|
|
5
|
+
* Cloudflare Stream Runtime Module
|
|
6
|
+
*
|
|
7
|
+
* This module is imported at runtime by the media provider system.
|
|
8
|
+
* It contains the actual provider implementation that interacts with the Cloudflare API.
|
|
9
|
+
*/
|
|
10
|
+
/** Safely extract a string from an unknown value */
|
|
11
|
+
function toString(value) {
|
|
12
|
+
return typeof value === "string" ? value : void 0;
|
|
13
|
+
}
|
|
14
|
+
/** Type guard: check if value is a record-like object */
|
|
15
|
+
function isRecord(value) {
|
|
16
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Resolve a config value, checking env var if direct value not provided
|
|
20
|
+
*/
|
|
21
|
+
function resolveEnvValue(directValue, envVarName, defaultEnvVar, serviceName) {
|
|
22
|
+
if (directValue) return directValue;
|
|
23
|
+
const envVar = envVarName || defaultEnvVar;
|
|
24
|
+
const value = env[envVar];
|
|
25
|
+
if (!value) throw new Error(`${serviceName}: Missing ${envVar}. Set it as an environment variable or provide it directly in config.`);
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Runtime implementation for Cloudflare Stream provider
|
|
30
|
+
*/
|
|
31
|
+
const createMediaProvider = (config) => {
|
|
32
|
+
const { customerSubdomain, controls = true, autoplay = false, loop = false, muted } = config;
|
|
33
|
+
const accountId = resolveEnvValue(config.accountId, config.accountIdEnvVar, "CF_ACCOUNT_ID", "Cloudflare Stream");
|
|
34
|
+
const apiToken = resolveEnvValue(config.apiToken, config.apiTokenEnvVar, "CF_STREAM_TOKEN", "Cloudflare Stream");
|
|
35
|
+
const apiBase = `https://api.cloudflare.com/client/v4/accounts/${accountId}/stream`;
|
|
36
|
+
const headers = { Authorization: `Bearer ${apiToken}` };
|
|
37
|
+
const isMuted = muted ?? autoplay;
|
|
38
|
+
return {
|
|
39
|
+
async list(options) {
|
|
40
|
+
const params = new URLSearchParams();
|
|
41
|
+
if (options.cursor) params.set("after", options.cursor);
|
|
42
|
+
params.set("asc", "false");
|
|
43
|
+
if (options.query) params.set("search", options.query);
|
|
44
|
+
const url = `${apiBase}?${params}`;
|
|
45
|
+
const response = await fetch(url, { headers });
|
|
46
|
+
if (!response.ok) throw new Error(`Cloudflare Stream API error: ${response.status}`);
|
|
47
|
+
const data = await response.json();
|
|
48
|
+
if (!data.success) throw new Error(`Cloudflare Stream API error: ${data.errors?.[0]?.message || "Unknown error"}`);
|
|
49
|
+
const nextCursor = data.result.at(-1)?.uid;
|
|
50
|
+
return {
|
|
51
|
+
items: data.result.map((video) => ({
|
|
52
|
+
id: video.uid,
|
|
53
|
+
filename: toString(video.meta?.name) || video.uid,
|
|
54
|
+
mimeType: "video/mp4",
|
|
55
|
+
width: video.input?.width,
|
|
56
|
+
height: video.input?.height,
|
|
57
|
+
previewUrl: video.thumbnail,
|
|
58
|
+
meta: {
|
|
59
|
+
duration: video.duration,
|
|
60
|
+
playback: video.playback,
|
|
61
|
+
status: video.status,
|
|
62
|
+
created: video.created,
|
|
63
|
+
modified: video.modified,
|
|
64
|
+
size: video.size
|
|
65
|
+
}
|
|
66
|
+
})),
|
|
67
|
+
nextCursor: data.result.length > 0 ? nextCursor : void 0
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
async get(id) {
|
|
71
|
+
const url = `${apiBase}/${id}`;
|
|
72
|
+
const response = await fetch(url, { headers });
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
if (response.status === 404) return null;
|
|
75
|
+
throw new Error(`Cloudflare Stream API error: ${response.status}`);
|
|
76
|
+
}
|
|
77
|
+
const data = await response.json();
|
|
78
|
+
if (!data.success) return null;
|
|
79
|
+
const video = data.result;
|
|
80
|
+
return {
|
|
81
|
+
id: video.uid,
|
|
82
|
+
filename: toString(video.meta?.name) || video.uid,
|
|
83
|
+
mimeType: "video/mp4",
|
|
84
|
+
width: video.input?.width,
|
|
85
|
+
height: video.input?.height,
|
|
86
|
+
previewUrl: video.thumbnail,
|
|
87
|
+
meta: {
|
|
88
|
+
duration: video.duration,
|
|
89
|
+
playback: video.playback,
|
|
90
|
+
status: video.status,
|
|
91
|
+
created: video.created,
|
|
92
|
+
modified: video.modified,
|
|
93
|
+
size: video.size
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
async upload(input) {
|
|
98
|
+
const createResponse = await fetch(`${apiBase}/direct_upload`, {
|
|
99
|
+
method: "POST",
|
|
100
|
+
headers: {
|
|
101
|
+
...headers,
|
|
102
|
+
"Content-Type": "application/json"
|
|
103
|
+
},
|
|
104
|
+
body: JSON.stringify({
|
|
105
|
+
maxDurationSeconds: 3600,
|
|
106
|
+
meta: { name: input.filename }
|
|
107
|
+
})
|
|
108
|
+
});
|
|
109
|
+
if (!createResponse.ok) {
|
|
110
|
+
const error = await createResponse.text();
|
|
111
|
+
throw new Error(`Failed to create upload URL: ${error}`);
|
|
112
|
+
}
|
|
113
|
+
const createData = await createResponse.json();
|
|
114
|
+
if (!createData.success) throw new Error(`Failed to create upload URL: ${createData.errors?.[0]?.message || "Unknown error"}`);
|
|
115
|
+
const uploadUrl = createData.result.uploadURL;
|
|
116
|
+
const formData = new FormData();
|
|
117
|
+
formData.append("file", input.file, input.filename);
|
|
118
|
+
const uploadResponse = await fetch(uploadUrl, {
|
|
119
|
+
method: "POST",
|
|
120
|
+
body: formData
|
|
121
|
+
});
|
|
122
|
+
if (!uploadResponse.ok) {
|
|
123
|
+
const error = await uploadResponse.text();
|
|
124
|
+
throw new Error(`Upload failed: ${error}`);
|
|
125
|
+
}
|
|
126
|
+
const videoId = createData.result.uid;
|
|
127
|
+
let video = null;
|
|
128
|
+
for (let i = 0; i < 10; i++) {
|
|
129
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
130
|
+
const checkResponse = await fetch(`${apiBase}/${videoId}`, { headers });
|
|
131
|
+
if (checkResponse.ok) {
|
|
132
|
+
const checkData = await checkResponse.json();
|
|
133
|
+
if (checkData.success && checkData.result.status?.state !== "queued") {
|
|
134
|
+
video = checkData.result;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (!video) return {
|
|
140
|
+
id: videoId,
|
|
141
|
+
filename: input.filename,
|
|
142
|
+
mimeType: "video/mp4",
|
|
143
|
+
previewUrl: void 0,
|
|
144
|
+
meta: { status: { state: "processing" } }
|
|
145
|
+
};
|
|
146
|
+
return {
|
|
147
|
+
id: video.uid,
|
|
148
|
+
filename: toString(video.meta?.name) || input.filename,
|
|
149
|
+
mimeType: "video/mp4",
|
|
150
|
+
width: video.input?.width,
|
|
151
|
+
height: video.input?.height,
|
|
152
|
+
previewUrl: video.thumbnail,
|
|
153
|
+
meta: {
|
|
154
|
+
duration: video.duration,
|
|
155
|
+
playback: video.playback,
|
|
156
|
+
status: video.status
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
},
|
|
160
|
+
async delete(id) {
|
|
161
|
+
const response = await fetch(`${apiBase}/${id}`, {
|
|
162
|
+
method: "DELETE",
|
|
163
|
+
headers
|
|
164
|
+
});
|
|
165
|
+
if (!response.ok && response.status !== 404) throw new Error(`Cloudflare Stream delete failed: ${response.status}`);
|
|
166
|
+
},
|
|
167
|
+
getEmbed(value, options) {
|
|
168
|
+
const rawPlayback = value.meta?.playback;
|
|
169
|
+
const playback = isRecord(rawPlayback) ? rawPlayback : void 0;
|
|
170
|
+
const hlsSrc = toString(playback?.hls);
|
|
171
|
+
const dashSrc = toString(playback?.dash);
|
|
172
|
+
if (hlsSrc) return {
|
|
173
|
+
type: "video",
|
|
174
|
+
sources: [{
|
|
175
|
+
src: hlsSrc,
|
|
176
|
+
type: "application/x-mpegURL"
|
|
177
|
+
}, ...dashSrc ? [{
|
|
178
|
+
src: dashSrc,
|
|
179
|
+
type: "application/dash+xml"
|
|
180
|
+
}] : []],
|
|
181
|
+
poster: toString(value.meta?.thumbnail),
|
|
182
|
+
width: options?.width ?? value.width,
|
|
183
|
+
height: options?.height ?? value.height,
|
|
184
|
+
controls,
|
|
185
|
+
autoplay,
|
|
186
|
+
loop,
|
|
187
|
+
muted: isMuted,
|
|
188
|
+
playsinline: true,
|
|
189
|
+
preload: "metadata"
|
|
190
|
+
};
|
|
191
|
+
const baseUrl = customerSubdomain ? `https://${customerSubdomain}` : `https://customer-${accountId.slice(0, 8)}.cloudflarestream.com`;
|
|
192
|
+
return {
|
|
193
|
+
type: "video",
|
|
194
|
+
src: `${baseUrl}/${value.id}/manifest/video.m3u8`,
|
|
195
|
+
poster: `${baseUrl}/${value.id}/thumbnails/thumbnail.jpg`,
|
|
196
|
+
width: options?.width ?? value.width,
|
|
197
|
+
height: options?.height ?? value.height,
|
|
198
|
+
controls,
|
|
199
|
+
autoplay,
|
|
200
|
+
loop,
|
|
201
|
+
muted: isMuted,
|
|
202
|
+
playsinline: true,
|
|
203
|
+
preload: "metadata"
|
|
204
|
+
};
|
|
205
|
+
},
|
|
206
|
+
getThumbnailUrl(id, _mimeType, options) {
|
|
207
|
+
const baseUrl = customerSubdomain ? `https://${customerSubdomain}` : `https://customer-${accountId.slice(0, 8)}.cloudflarestream.com`;
|
|
208
|
+
const width = options?.width || 400;
|
|
209
|
+
const height = options?.height;
|
|
210
|
+
let url = `${baseUrl}/${id}/thumbnails/thumbnail.jpg?width=${width}`;
|
|
211
|
+
if (height) url += `&height=${height}`;
|
|
212
|
+
return url;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
//#endregion
|
|
218
|
+
export { createMediaProvider };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { PluginDefinition } from "emdash";
|
|
2
|
+
|
|
3
|
+
//#region src/plugins/vectorize-search.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Vectorize Search Plugin Configuration
|
|
6
|
+
*/
|
|
7
|
+
interface VectorizeSearchConfig {
|
|
8
|
+
/**
|
|
9
|
+
* Name of the Vectorize index
|
|
10
|
+
* @default "emdash-content"
|
|
11
|
+
*/
|
|
12
|
+
indexName?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Workers AI embedding model to use
|
|
15
|
+
* @default "@cf/bge-base-en-v1.5"
|
|
16
|
+
*/
|
|
17
|
+
model?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Collections to index. If not specified, indexes all collections
|
|
20
|
+
* that have search enabled in their config.
|
|
21
|
+
*/
|
|
22
|
+
collections?: string[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Create a Vectorize Search plugin definition
|
|
26
|
+
*
|
|
27
|
+
* Note: This returns a plain plugin definition object, not a resolved plugin.
|
|
28
|
+
* It should be passed to the emdash() integration's plugins array.
|
|
29
|
+
*/
|
|
30
|
+
declare function vectorizeSearch(config?: VectorizeSearchConfig): PluginDefinition;
|
|
31
|
+
//#endregion
|
|
32
|
+
export { type VectorizeSearchConfig, vectorizeSearch };
|