@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
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 };