@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.
Files changed (67) hide show
  1. package/dist/auth/index.d.mts +81 -0
  2. package/dist/auth/index.mjs +147 -0
  3. package/dist/cache/config.d.mts +52 -0
  4. package/dist/cache/config.mjs +55 -0
  5. package/dist/cache/runtime.d.mts +40 -0
  6. package/dist/cache/runtime.mjs +191 -0
  7. package/dist/d1-introspector-bZf0_ylK.mjs +57 -0
  8. package/dist/db/d1.d.mts +43 -0
  9. package/dist/db/d1.mjs +74 -0
  10. package/dist/db/do.d.mts +96 -0
  11. package/dist/db/do.mjs +489 -0
  12. package/dist/db/playground-middleware.d.mts +20 -0
  13. package/dist/db/playground-middleware.mjs +533 -0
  14. package/dist/db/playground.d.mts +39 -0
  15. package/dist/db/playground.mjs +26 -0
  16. package/dist/do-class-DY2Ba2RJ.mjs +174 -0
  17. package/dist/do-class-x5Xh_G62.d.mts +73 -0
  18. package/dist/do-dialect-BhFcRSFQ.mjs +58 -0
  19. package/dist/do-playground-routes-CmwFeGwJ.mjs +49 -0
  20. package/dist/do-types-CY0G0oyh.d.mts +14 -0
  21. package/dist/images-4RT9Ag8_.d.mts +76 -0
  22. package/dist/index.d.mts +200 -0
  23. package/dist/index.mjs +214 -0
  24. package/dist/media/images-runtime.d.mts +10 -0
  25. package/dist/media/images-runtime.mjs +215 -0
  26. package/dist/media/stream-runtime.d.mts +10 -0
  27. package/dist/media/stream-runtime.mjs +218 -0
  28. package/dist/plugins/index.d.mts +32 -0
  29. package/dist/plugins/index.mjs +163 -0
  30. package/dist/sandbox/index.d.mts +255 -0
  31. package/dist/sandbox/index.mjs +945 -0
  32. package/dist/storage/r2.d.mts +31 -0
  33. package/dist/storage/r2.mjs +116 -0
  34. package/dist/stream-DdbcvKi0.d.mts +78 -0
  35. package/package.json +109 -0
  36. package/src/auth/cloudflare-access.ts +303 -0
  37. package/src/auth/index.ts +16 -0
  38. package/src/cache/config.ts +81 -0
  39. package/src/cache/runtime.ts +328 -0
  40. package/src/cloudflare.d.ts +31 -0
  41. package/src/db/d1-introspector.ts +120 -0
  42. package/src/db/d1.ts +112 -0
  43. package/src/db/do-class.ts +275 -0
  44. package/src/db/do-dialect.ts +125 -0
  45. package/src/db/do-playground-routes.ts +65 -0
  46. package/src/db/do-preview-routes.ts +48 -0
  47. package/src/db/do-preview-sign.ts +100 -0
  48. package/src/db/do-preview.ts +268 -0
  49. package/src/db/do-types.ts +12 -0
  50. package/src/db/do.ts +62 -0
  51. package/src/db/playground-middleware.ts +340 -0
  52. package/src/db/playground-toolbar.ts +341 -0
  53. package/src/db/playground.ts +49 -0
  54. package/src/db/preview-toolbar.ts +220 -0
  55. package/src/index.ts +285 -0
  56. package/src/media/images-runtime.ts +353 -0
  57. package/src/media/images.ts +114 -0
  58. package/src/media/stream-runtime.ts +392 -0
  59. package/src/media/stream.ts +118 -0
  60. package/src/plugins/index.ts +7 -0
  61. package/src/plugins/vectorize-search.ts +393 -0
  62. package/src/sandbox/bridge.ts +1008 -0
  63. package/src/sandbox/index.ts +13 -0
  64. package/src/sandbox/runner.ts +357 -0
  65. package/src/sandbox/types.ts +181 -0
  66. package/src/sandbox/wrapper.ts +238 -0
  67. package/src/storage/r2.ts +200 -0
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Cloudflare Images Media Provider
3
+ *
4
+ * Provides integration with Cloudflare Images for image hosting and transformation.
5
+ *
6
+ * Features:
7
+ * - Browse uploaded images
8
+ * - Upload new images
9
+ * - Delete images
10
+ * - URL-based image transformations (resize, format conversion, etc.)
11
+ *
12
+ * @see https://developers.cloudflare.com/images/
13
+ */
14
+
15
+ import type { MediaProviderDescriptor } from "emdash/media";
16
+
17
+ /**
18
+ * Cloudflare Images configuration
19
+ */
20
+ export interface CloudflareImagesConfig {
21
+ /**
22
+ * Cloudflare Account ID (for API calls)
23
+ * If not provided, reads from accountIdEnvVar at runtime
24
+ */
25
+ accountId?: string;
26
+
27
+ /**
28
+ * Environment variable name containing the Account ID
29
+ * @default "CF_ACCOUNT_ID"
30
+ */
31
+ accountIdEnvVar?: string;
32
+
33
+ /**
34
+ * Cloudflare Images Account Hash (for delivery URLs)
35
+ * This is different from the Account ID - find it in the Cloudflare dashboard
36
+ * under Images > Overview > "Account Hash"
37
+ * If not provided, reads from accountHashEnvVar at runtime
38
+ */
39
+ accountHash?: string;
40
+
41
+ /**
42
+ * Environment variable name containing the Account Hash
43
+ * @default "CF_IMAGES_ACCOUNT_HASH"
44
+ */
45
+ accountHashEnvVar?: string;
46
+
47
+ /**
48
+ * API Token with Images permissions
49
+ * If not provided, reads from apiTokenEnvVar at runtime
50
+ * Should have "Cloudflare Images: Read" and "Cloudflare Images: Edit" permissions
51
+ */
52
+ apiToken?: string;
53
+
54
+ /**
55
+ * Environment variable name containing the API token
56
+ * @default "CF_IMAGES_TOKEN"
57
+ */
58
+ apiTokenEnvVar?: string;
59
+
60
+ /**
61
+ * Custom delivery domain (optional)
62
+ * If not specified, uses imagedelivery.net
63
+ * @example "images.example.com"
64
+ */
65
+ deliveryDomain?: string;
66
+
67
+ /**
68
+ * Default variant to use for display
69
+ * @default "public"
70
+ */
71
+ defaultVariant?: string;
72
+ }
73
+
74
+ // Cloudflare Images icon (inline SVG as data URL)
75
+ 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>')}`;
76
+
77
+ /**
78
+ * Cloudflare Images media provider
79
+ *
80
+ * @example
81
+ * ```ts
82
+ * import { cloudflareImages } from "@emdash-cms/cloudflare";
83
+ *
84
+ * emdash({
85
+ * mediaProviders: [
86
+ * // Uses CF_ACCOUNT_ID and CF_IMAGES_TOKEN env vars by default
87
+ * cloudflareImages({}),
88
+ *
89
+ * // Or with custom env var names
90
+ * cloudflareImages({
91
+ * accountIdEnvVar: "MY_CF_ACCOUNT",
92
+ * apiTokenEnvVar: "MY_CF_IMAGES_KEY",
93
+ * }),
94
+ * ],
95
+ * })
96
+ * ```
97
+ */
98
+ export function cloudflareImages(
99
+ config: CloudflareImagesConfig,
100
+ ): MediaProviderDescriptor<CloudflareImagesConfig> {
101
+ return {
102
+ id: "cloudflare-images",
103
+ name: "Cloudflare Images",
104
+ icon: IMAGES_ICON,
105
+ entrypoint: "@emdash-cms/cloudflare/media/images-runtime",
106
+ capabilities: {
107
+ browse: true,
108
+ search: false, // Images API doesn't support search
109
+ upload: true,
110
+ delete: true,
111
+ },
112
+ config,
113
+ };
114
+ }
@@ -0,0 +1,392 @@
1
+ /**
2
+ * Cloudflare Stream Runtime Module
3
+ *
4
+ * This module is imported at runtime by the media provider system.
5
+ * It contains the actual provider implementation that interacts with the Cloudflare API.
6
+ */
7
+
8
+ import { env } from "cloudflare:workers";
9
+ import type {
10
+ MediaProvider,
11
+ MediaListOptions,
12
+ MediaValue,
13
+ EmbedOptions,
14
+ EmbedResult,
15
+ CreateMediaProviderFn,
16
+ } from "emdash/media";
17
+
18
+ import type { CloudflareStreamConfig } from "./stream.js";
19
+
20
+ /** Safely extract a string from an unknown value */
21
+ function toString(value: unknown): string | undefined {
22
+ return typeof value === "string" ? value : undefined;
23
+ }
24
+
25
+ /** Type guard: check if value is a record-like object */
26
+ function isRecord(value: unknown): value is Record<string, unknown> {
27
+ return value != null && typeof value === "object" && !Array.isArray(value);
28
+ }
29
+
30
+ /**
31
+ * Resolve a config value, checking env var if direct value not provided
32
+ */
33
+ function resolveEnvValue(
34
+ directValue: string | undefined,
35
+ envVarName: string | undefined,
36
+ defaultEnvVar: string,
37
+ serviceName: string,
38
+ ): string {
39
+ if (directValue) return directValue;
40
+ const envVar = envVarName || defaultEnvVar;
41
+ // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Worker binding accessed from untyped env object
42
+ const value = (env as Record<string, string | undefined>)[envVar];
43
+ if (!value) {
44
+ throw new Error(
45
+ `${serviceName}: Missing ${envVar}. Set it as an environment variable or provide it directly in config.`,
46
+ );
47
+ }
48
+ return value;
49
+ }
50
+
51
+ /**
52
+ * Runtime implementation for Cloudflare Stream provider
53
+ */
54
+ export const createMediaProvider: CreateMediaProviderFn<CloudflareStreamConfig> = (config) => {
55
+ const { customerSubdomain, controls = true, autoplay = false, loop = false, muted } = config;
56
+
57
+ // Resolve credentials from config or env vars
58
+ const accountId = resolveEnvValue(
59
+ config.accountId,
60
+ config.accountIdEnvVar,
61
+ "CF_ACCOUNT_ID",
62
+ "Cloudflare Stream",
63
+ );
64
+ const apiToken = resolveEnvValue(
65
+ config.apiToken,
66
+ config.apiTokenEnvVar,
67
+ "CF_STREAM_TOKEN",
68
+ "Cloudflare Stream",
69
+ );
70
+
71
+ const apiBase = `https://api.cloudflare.com/client/v4/accounts/${accountId}/stream`;
72
+ const headers = { Authorization: `Bearer ${apiToken}` };
73
+
74
+ // Muted defaults to true if autoplay is enabled (browser requirement)
75
+ const isMuted = muted ?? autoplay;
76
+
77
+ const provider: MediaProvider = {
78
+ async list(options: MediaListOptions) {
79
+ const params = new URLSearchParams();
80
+
81
+ // Stream uses "after" for cursor-based pagination
82
+ if (options.cursor) {
83
+ params.set("after", options.cursor);
84
+ }
85
+
86
+ // Stream uses "asc" boolean, default is newest first
87
+ params.set("asc", "false");
88
+
89
+ // Search by name if query provided
90
+ if (options.query) {
91
+ params.set("search", options.query);
92
+ }
93
+
94
+ const url = `${apiBase}?${params}`;
95
+ const response = await fetch(url, { headers });
96
+
97
+ if (!response.ok) {
98
+ throw new Error(`Cloudflare Stream API error: ${response.status}`);
99
+ }
100
+
101
+ const data: CloudflareStreamListResponse = await response.json();
102
+
103
+ if (!data.success) {
104
+ throw new Error(
105
+ `Cloudflare Stream API error: ${data.errors?.[0]?.message || "Unknown error"}`,
106
+ );
107
+ }
108
+
109
+ // Get the last video's UID for cursor-based pagination
110
+ const lastVideo = data.result.at(-1);
111
+ const nextCursor = lastVideo?.uid;
112
+
113
+ return {
114
+ items: data.result.map((video) => ({
115
+ id: video.uid,
116
+ filename: toString(video.meta?.name) || video.uid,
117
+ mimeType: "video/mp4",
118
+ width: video.input?.width,
119
+ height: video.input?.height,
120
+ previewUrl: video.thumbnail,
121
+ meta: {
122
+ duration: video.duration,
123
+ playback: video.playback,
124
+ status: video.status,
125
+ created: video.created,
126
+ modified: video.modified,
127
+ size: video.size,
128
+ },
129
+ })),
130
+ nextCursor: data.result.length > 0 ? nextCursor : undefined,
131
+ };
132
+ },
133
+
134
+ async get(id: string) {
135
+ const url = `${apiBase}/${id}`;
136
+ const response = await fetch(url, { headers });
137
+
138
+ if (!response.ok) {
139
+ if (response.status === 404) return null;
140
+ throw new Error(`Cloudflare Stream API error: ${response.status}`);
141
+ }
142
+
143
+ const data: CloudflareStreamResponse = await response.json();
144
+
145
+ if (!data.success) {
146
+ return null;
147
+ }
148
+
149
+ const video = data.result;
150
+ return {
151
+ id: video.uid,
152
+ filename: toString(video.meta?.name) || video.uid,
153
+ mimeType: "video/mp4",
154
+ width: video.input?.width,
155
+ height: video.input?.height,
156
+ previewUrl: video.thumbnail,
157
+ meta: {
158
+ duration: video.duration,
159
+ playback: video.playback,
160
+ status: video.status,
161
+ created: video.created,
162
+ modified: video.modified,
163
+ size: video.size,
164
+ },
165
+ };
166
+ },
167
+
168
+ async upload(input) {
169
+ // Stream supports tus protocol for resumable uploads
170
+ // For simplicity, we'll use direct creator upload which creates an upload URL
171
+ // For large files, this would need to be enhanced with tus
172
+
173
+ // First, create a direct upload URL
174
+ const createResponse = await fetch(`${apiBase}/direct_upload`, {
175
+ method: "POST",
176
+ headers: {
177
+ ...headers,
178
+ "Content-Type": "application/json",
179
+ },
180
+ body: JSON.stringify({
181
+ maxDurationSeconds: 3600, // 1 hour max
182
+ meta: {
183
+ name: input.filename,
184
+ },
185
+ }),
186
+ });
187
+
188
+ if (!createResponse.ok) {
189
+ const error = await createResponse.text();
190
+ throw new Error(`Failed to create upload URL: ${error}`);
191
+ }
192
+
193
+ const createData: CloudflareStreamDirectUploadResponse = await createResponse.json();
194
+
195
+ if (!createData.success) {
196
+ throw new Error(
197
+ `Failed to create upload URL: ${createData.errors?.[0]?.message || "Unknown error"}`,
198
+ );
199
+ }
200
+
201
+ // Upload the file to the provided URL
202
+ const uploadUrl = createData.result.uploadURL;
203
+ const formData = new FormData();
204
+ formData.append("file", input.file, input.filename);
205
+
206
+ const uploadResponse = await fetch(uploadUrl, {
207
+ method: "POST",
208
+ body: formData,
209
+ });
210
+
211
+ if (!uploadResponse.ok) {
212
+ const error = await uploadResponse.text();
213
+ throw new Error(`Upload failed: ${error}`);
214
+ }
215
+
216
+ // The upload response contains the video details
217
+ // Wait a moment for the video to be processed
218
+ const videoId = createData.result.uid;
219
+
220
+ // Poll for the video to be ready (simple implementation)
221
+ let video: CloudflareStreamVideo | null = null;
222
+ for (let i = 0; i < 10; i++) {
223
+ await new Promise((resolve) => setTimeout(resolve, 1000));
224
+
225
+ const checkResponse = await fetch(`${apiBase}/${videoId}`, { headers });
226
+ if (checkResponse.ok) {
227
+ const checkData: CloudflareStreamResponse = await checkResponse.json();
228
+ if (checkData.success && checkData.result.status?.state !== "queued") {
229
+ video = checkData.result;
230
+ break;
231
+ }
232
+ }
233
+ }
234
+
235
+ if (!video) {
236
+ // Return with pending status - thumbnail might not be ready yet
237
+ return {
238
+ id: videoId,
239
+ filename: input.filename,
240
+ mimeType: "video/mp4",
241
+ previewUrl: undefined,
242
+ meta: {
243
+ status: { state: "processing" },
244
+ },
245
+ };
246
+ }
247
+
248
+ return {
249
+ id: video.uid,
250
+ filename: toString(video.meta?.name) || input.filename,
251
+ mimeType: "video/mp4",
252
+ width: video.input?.width,
253
+ height: video.input?.height,
254
+ previewUrl: video.thumbnail,
255
+ meta: {
256
+ duration: video.duration,
257
+ playback: video.playback,
258
+ status: video.status,
259
+ },
260
+ };
261
+ },
262
+
263
+ async delete(id: string) {
264
+ const response = await fetch(`${apiBase}/${id}`, {
265
+ method: "DELETE",
266
+ headers,
267
+ });
268
+
269
+ if (!response.ok && response.status !== 404) {
270
+ throw new Error(`Cloudflare Stream delete failed: ${response.status}`);
271
+ }
272
+ },
273
+
274
+ getEmbed(value: MediaValue, options?: EmbedOptions): EmbedResult {
275
+ const rawPlayback = value.meta?.playback;
276
+ const playback = isRecord(rawPlayback) ? rawPlayback : undefined;
277
+
278
+ const hlsSrc = toString(playback?.hls);
279
+ const dashSrc = toString(playback?.dash);
280
+
281
+ // Build the Stream player iframe URL or use HLS/DASH directly
282
+ // For video embeds, we can use the HLS stream URL
283
+ if (hlsSrc) {
284
+ return {
285
+ type: "video",
286
+ sources: [
287
+ { src: hlsSrc, type: "application/x-mpegURL" },
288
+ ...(dashSrc ? [{ src: dashSrc, type: "application/dash+xml" }] : []),
289
+ ],
290
+ poster: toString(value.meta?.thumbnail),
291
+ width: options?.width ?? value.width,
292
+ height: options?.height ?? value.height,
293
+ controls,
294
+ autoplay,
295
+ loop,
296
+ muted: isMuted,
297
+ playsinline: true,
298
+ preload: "metadata",
299
+ };
300
+ }
301
+
302
+ // Fallback: use the Stream embed player URL
303
+ const baseUrl = customerSubdomain
304
+ ? `https://${customerSubdomain}`
305
+ : `https://customer-${accountId.slice(0, 8)}.cloudflarestream.com`;
306
+
307
+ return {
308
+ type: "video",
309
+ src: `${baseUrl}/${value.id}/manifest/video.m3u8`,
310
+ poster: `${baseUrl}/${value.id}/thumbnails/thumbnail.jpg`,
311
+ width: options?.width ?? value.width,
312
+ height: options?.height ?? value.height,
313
+ controls,
314
+ autoplay,
315
+ loop,
316
+ muted: isMuted,
317
+ playsinline: true,
318
+ preload: "metadata",
319
+ };
320
+ },
321
+
322
+ getThumbnailUrl(id: string, _mimeType?: string, options?: { width?: number; height?: number }) {
323
+ // For videos, return a thumbnail/poster image
324
+ const baseUrl = customerSubdomain
325
+ ? `https://${customerSubdomain}`
326
+ : `https://customer-${accountId.slice(0, 8)}.cloudflarestream.com`;
327
+
328
+ // Stream supports thumbnail customization via URL params
329
+ const width = options?.width || 400;
330
+ const height = options?.height;
331
+ let url = `${baseUrl}/${id}/thumbnails/thumbnail.jpg?width=${width}`;
332
+ if (height) url += `&height=${height}`;
333
+ return url;
334
+ },
335
+ };
336
+
337
+ return provider;
338
+ };
339
+
340
+ // Cloudflare Stream API response types
341
+ interface CloudflareStreamListResponse {
342
+ success: boolean;
343
+ errors?: Array<{ message: string }>;
344
+ result: CloudflareStreamVideo[];
345
+ }
346
+
347
+ interface CloudflareStreamResponse {
348
+ success: boolean;
349
+ errors?: Array<{ message: string }>;
350
+ result: CloudflareStreamVideo;
351
+ }
352
+
353
+ interface CloudflareStreamDirectUploadResponse {
354
+ success: boolean;
355
+ errors?: Array<{ message: string }>;
356
+ result: {
357
+ uploadURL: string;
358
+ uid: string;
359
+ };
360
+ }
361
+
362
+ interface CloudflareStreamVideo {
363
+ uid: string;
364
+ thumbnail: string;
365
+ thumbnailTimestampPct?: number;
366
+ readyToStream: boolean;
367
+ status: {
368
+ state: string;
369
+ pctComplete?: string;
370
+ errorReasonCode?: string;
371
+ errorReasonText?: string;
372
+ };
373
+ meta?: Record<string, unknown>;
374
+ created: string;
375
+ modified: string;
376
+ size: number;
377
+ preview?: string;
378
+ allowedOrigins?: string[];
379
+ requireSignedURLs: boolean;
380
+ uploaded?: string;
381
+ scheduledDeletion?: string;
382
+ input?: {
383
+ width: number;
384
+ height: number;
385
+ };
386
+ playback?: {
387
+ hls: string;
388
+ dash: string;
389
+ };
390
+ watermark?: unknown;
391
+ duration: number;
392
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Cloudflare Stream Media Provider
3
+ *
4
+ * Provides integration with Cloudflare Stream for video hosting and streaming.
5
+ *
6
+ * Features:
7
+ * - Browse uploaded videos
8
+ * - Upload new videos (direct upload)
9
+ * - Delete videos
10
+ * - HLS/DASH streaming URLs
11
+ * - Thumbnail generation
12
+ *
13
+ * @see https://developers.cloudflare.com/stream/
14
+ */
15
+
16
+ import type { MediaProviderDescriptor } from "emdash/media";
17
+
18
+ /**
19
+ * Cloudflare Stream configuration
20
+ */
21
+ export interface CloudflareStreamConfig {
22
+ /**
23
+ * Cloudflare Account ID
24
+ * If not provided, reads from accountIdEnvVar at runtime
25
+ */
26
+ accountId?: string;
27
+
28
+ /**
29
+ * Environment variable name containing the Account ID
30
+ * @default "CF_ACCOUNT_ID"
31
+ */
32
+ accountIdEnvVar?: string;
33
+
34
+ /**
35
+ * API Token with Stream permissions
36
+ * If not provided, reads from apiTokenEnvVar at runtime
37
+ * Should have "Stream: Read" and "Stream: Edit" permissions
38
+ */
39
+ apiToken?: string;
40
+
41
+ /**
42
+ * Environment variable name containing the API token
43
+ * @default "CF_STREAM_TOKEN"
44
+ */
45
+ apiTokenEnvVar?: string;
46
+
47
+ /**
48
+ * Customer subdomain for Stream delivery (optional)
49
+ * If not provided, uses customer-{hash}.cloudflarestream.com format
50
+ */
51
+ customerSubdomain?: string;
52
+
53
+ /**
54
+ * Default player controls setting
55
+ * @default true
56
+ */
57
+ controls?: boolean;
58
+
59
+ /**
60
+ * Autoplay videos (muted by default to comply with browser policies)
61
+ * @default false
62
+ */
63
+ autoplay?: boolean;
64
+
65
+ /**
66
+ * Loop videos
67
+ * @default false
68
+ */
69
+ loop?: boolean;
70
+
71
+ /**
72
+ * Mute videos
73
+ * @default false (true if autoplay is enabled)
74
+ */
75
+ muted?: boolean;
76
+ }
77
+
78
+ // Cloudflare Stream icon (inline SVG as data URL)
79
+ 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>')}`;
80
+
81
+ /**
82
+ * Cloudflare Stream media provider
83
+ *
84
+ * @example
85
+ * ```ts
86
+ * import { cloudflareStream } from "@emdash-cms/cloudflare";
87
+ *
88
+ * emdash({
89
+ * mediaProviders: [
90
+ * // Uses CF_ACCOUNT_ID and CF_STREAM_TOKEN env vars by default
91
+ * cloudflareStream({}),
92
+ *
93
+ * // Or with custom env var names
94
+ * cloudflareStream({
95
+ * accountIdEnvVar: "MY_CF_ACCOUNT",
96
+ * apiTokenEnvVar: "MY_CF_STREAM_KEY",
97
+ * }),
98
+ * ],
99
+ * })
100
+ * ```
101
+ */
102
+ export function cloudflareStream(
103
+ config: CloudflareStreamConfig,
104
+ ): MediaProviderDescriptor<CloudflareStreamConfig> {
105
+ return {
106
+ id: "cloudflare-stream",
107
+ name: "Cloudflare Stream",
108
+ icon: STREAM_ICON,
109
+ entrypoint: "@emdash-cms/cloudflare/media/stream-runtime",
110
+ capabilities: {
111
+ browse: true,
112
+ search: true,
113
+ upload: true,
114
+ delete: true,
115
+ },
116
+ config,
117
+ };
118
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Cloudflare Plugins
3
+ *
4
+ * Optional plugins that enhance EmDash with Cloudflare-specific features.
5
+ */
6
+
7
+ export { vectorizeSearch, type VectorizeSearchConfig } from "./vectorize-search.js";