@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/src/index.ts
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @emdash-cms/cloudflare
|
|
3
|
+
*
|
|
4
|
+
* Cloudflare adapters for EmDash:
|
|
5
|
+
* - D1 database adapter
|
|
6
|
+
* - R2 storage adapter
|
|
7
|
+
* - Cloudflare Access authentication
|
|
8
|
+
* - Worker Loader sandbox for plugins
|
|
9
|
+
*
|
|
10
|
+
* This is the CONFIG-TIME entry point. It does NOT import cloudflare:workers
|
|
11
|
+
* and is safe to use in astro.config.mjs.
|
|
12
|
+
*
|
|
13
|
+
* For runtime exports (PluginBridge, authenticate), import from the specific
|
|
14
|
+
* runtime entrypoints:
|
|
15
|
+
* - @emdash-cms/cloudflare/sandbox (PluginBridge, createSandboxRunner)
|
|
16
|
+
* - @emdash-cms/cloudflare/auth (authenticate)
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* import emdash from "emdash/astro";
|
|
21
|
+
* import { d1, r2, access, sandbox } from "@emdash-cms/cloudflare";
|
|
22
|
+
*
|
|
23
|
+
* export default defineConfig({
|
|
24
|
+
* integrations: [
|
|
25
|
+
* emdash({
|
|
26
|
+
* database: d1({ binding: "DB" }),
|
|
27
|
+
* storage: r2({ binding: "MEDIA" }),
|
|
28
|
+
* auth: access({ teamDomain: "myteam.cloudflareaccess.com" }),
|
|
29
|
+
* sandboxRunner: sandbox(),
|
|
30
|
+
* }),
|
|
31
|
+
* ],
|
|
32
|
+
* });
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import type { AuthDescriptor, DatabaseDescriptor, StorageDescriptor } from "emdash";
|
|
37
|
+
|
|
38
|
+
import type { PreviewDOConfig } from "./db/do-types.js";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* D1 configuration
|
|
42
|
+
*/
|
|
43
|
+
export interface D1Config {
|
|
44
|
+
/**
|
|
45
|
+
* Name of the D1 binding in wrangler.toml
|
|
46
|
+
*/
|
|
47
|
+
binding: string;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Read replication session mode.
|
|
51
|
+
*
|
|
52
|
+
* - `"disabled"` — No sessions. All queries go to primary. (default)
|
|
53
|
+
* - `"auto"` — Automatic session management. Anonymous requests use
|
|
54
|
+
* `"first-unconstrained"` (nearest replica). Authenticated requests
|
|
55
|
+
* use bookmark cookies for read-your-writes consistency.
|
|
56
|
+
* - `"primary-first"` — Like `"auto"`, but the first query in every
|
|
57
|
+
* session goes to the primary. Use this if your site has very
|
|
58
|
+
* frequent writes and you need stronger consistency guarantees
|
|
59
|
+
* at the cost of higher read latency.
|
|
60
|
+
*
|
|
61
|
+
* Read replication must also be enabled on the D1 database itself
|
|
62
|
+
* (via dashboard or REST API).
|
|
63
|
+
*/
|
|
64
|
+
session?: "disabled" | "auto" | "primary-first";
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Cookie name for storing the session bookmark.
|
|
68
|
+
* Only used when session is `"auto"` or `"primary-first"`.
|
|
69
|
+
*
|
|
70
|
+
* @default "__ec_d1_bookmark"
|
|
71
|
+
*/
|
|
72
|
+
bookmarkCookie?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* R2 storage configuration
|
|
77
|
+
*/
|
|
78
|
+
export interface R2StorageConfig {
|
|
79
|
+
/**
|
|
80
|
+
* Name of the R2 binding in wrangler.toml
|
|
81
|
+
*/
|
|
82
|
+
binding: string;
|
|
83
|
+
/**
|
|
84
|
+
* Public URL for accessing files (optional CDN)
|
|
85
|
+
*/
|
|
86
|
+
publicUrl?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Configuration for Cloudflare Access authentication
|
|
91
|
+
*/
|
|
92
|
+
export interface AccessConfig {
|
|
93
|
+
/**
|
|
94
|
+
* Your Cloudflare Access team domain
|
|
95
|
+
* @example "myteam.cloudflareaccess.com"
|
|
96
|
+
*/
|
|
97
|
+
teamDomain: string;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Application Audience (AUD) tag from Access application settings.
|
|
101
|
+
* For Cloudflare Workers, use `audienceEnvVar` instead to read at runtime.
|
|
102
|
+
*/
|
|
103
|
+
audience?: string;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Environment variable name containing the audience tag.
|
|
107
|
+
* Read at runtime from environment.
|
|
108
|
+
* @default "CF_ACCESS_AUDIENCE"
|
|
109
|
+
*/
|
|
110
|
+
audienceEnvVar?: string;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Automatically create EmDash users on first login
|
|
114
|
+
* @default true
|
|
115
|
+
*/
|
|
116
|
+
autoProvision?: boolean;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Role level for users not matching any group in roleMapping
|
|
120
|
+
* @default 30 (Editor)
|
|
121
|
+
*/
|
|
122
|
+
defaultRole?: number;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Update user's role on each login based on current IdP groups
|
|
126
|
+
* When false, role is only set on first provisioning
|
|
127
|
+
* @default false
|
|
128
|
+
*/
|
|
129
|
+
syncRoles?: boolean;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Map IdP group names to EmDash role levels
|
|
133
|
+
* First match wins if user is in multiple groups
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```ts
|
|
137
|
+
* roleMapping: {
|
|
138
|
+
* "Admins": 50, // Admin
|
|
139
|
+
* "Developers": 40, // Developer
|
|
140
|
+
* "Content Team": 30, // Editor
|
|
141
|
+
* }
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
roleMapping?: Record<string, number>;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Cloudflare D1 database adapter
|
|
149
|
+
*
|
|
150
|
+
* For Cloudflare Workers with D1 binding.
|
|
151
|
+
* Migrations run automatically at setup time - no need for manual SQL files.
|
|
152
|
+
*
|
|
153
|
+
* Uses a custom introspector that works around D1's restriction on
|
|
154
|
+
* cross-joins with pragma_table_info().
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```ts
|
|
158
|
+
* database: d1({ binding: "DB" })
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
export function d1(config: D1Config): DatabaseDescriptor {
|
|
162
|
+
return {
|
|
163
|
+
entrypoint: "@emdash-cms/cloudflare/db/d1",
|
|
164
|
+
config,
|
|
165
|
+
type: "sqlite",
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export type { PreviewDOConfig } from "./db/do-types.js";
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Durable Object preview database adapter
|
|
173
|
+
*
|
|
174
|
+
* Each preview session gets an isolated SQLite database inside a DO,
|
|
175
|
+
* populated from a snapshot of the source EmDash site.
|
|
176
|
+
*
|
|
177
|
+
* Not for production use — preview only.
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* ```ts
|
|
181
|
+
* database: previewDatabase({ binding: "PREVIEW_DB" })
|
|
182
|
+
* ```
|
|
183
|
+
*/
|
|
184
|
+
export function previewDatabase(config: PreviewDOConfig): DatabaseDescriptor {
|
|
185
|
+
return {
|
|
186
|
+
entrypoint: "@emdash-cms/cloudflare/db/do",
|
|
187
|
+
config,
|
|
188
|
+
type: "sqlite",
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Durable Object playground database adapter
|
|
194
|
+
*
|
|
195
|
+
* Each playground session gets an isolated SQLite database inside a DO,
|
|
196
|
+
* populated from a seed file with migrations run at init time.
|
|
197
|
+
* Unlike preview, playground is writable and has admin access.
|
|
198
|
+
*
|
|
199
|
+
* Not for production use -- playground/demo only.
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* ```ts
|
|
203
|
+
* database: playgroundDatabase({ binding: "PLAYGROUND_DB" })
|
|
204
|
+
* ```
|
|
205
|
+
*/
|
|
206
|
+
export function playgroundDatabase(config: PreviewDOConfig): DatabaseDescriptor {
|
|
207
|
+
return {
|
|
208
|
+
entrypoint: "@emdash-cms/cloudflare/db/playground",
|
|
209
|
+
config,
|
|
210
|
+
type: "sqlite",
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Cloudflare R2 binding adapter
|
|
216
|
+
*
|
|
217
|
+
* Uses R2 bindings directly when running on Cloudflare Workers.
|
|
218
|
+
* Does NOT support signed upload URLs (use s3() with R2 credentials instead).
|
|
219
|
+
*
|
|
220
|
+
* Requires R2 binding in wrangler.toml:
|
|
221
|
+
* ```toml
|
|
222
|
+
* [[r2_buckets]]
|
|
223
|
+
* binding = "MEDIA"
|
|
224
|
+
* bucket_name = "my-media-bucket"
|
|
225
|
+
* ```
|
|
226
|
+
*
|
|
227
|
+
* @example
|
|
228
|
+
* ```ts
|
|
229
|
+
* storage: r2({ binding: "MEDIA" })
|
|
230
|
+
* ```
|
|
231
|
+
*/
|
|
232
|
+
export function r2(config: R2StorageConfig): StorageDescriptor {
|
|
233
|
+
return {
|
|
234
|
+
entrypoint: "@emdash-cms/cloudflare/storage/r2",
|
|
235
|
+
config: { binding: config.binding, publicUrl: config.publicUrl },
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Cloudflare Access authentication adapter
|
|
241
|
+
*
|
|
242
|
+
* Use this to configure EmDash to authenticate via Cloudflare Access.
|
|
243
|
+
* When Access is configured, passkey auth is disabled.
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* ```ts
|
|
247
|
+
* auth: access({
|
|
248
|
+
* teamDomain: "myteam.cloudflareaccess.com",
|
|
249
|
+
* audience: "abc123...",
|
|
250
|
+
* roleMapping: {
|
|
251
|
+
* "Admins": 50,
|
|
252
|
+
* "Editors": 30,
|
|
253
|
+
* },
|
|
254
|
+
* })
|
|
255
|
+
* ```
|
|
256
|
+
*/
|
|
257
|
+
export function access(config: AccessConfig): AuthDescriptor {
|
|
258
|
+
return {
|
|
259
|
+
type: "cloudflare-access",
|
|
260
|
+
entrypoint: "@emdash-cms/cloudflare/auth",
|
|
261
|
+
config,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Cloudflare Worker Loader sandbox adapter
|
|
267
|
+
*
|
|
268
|
+
* Returns the module path for the Cloudflare sandbox runner.
|
|
269
|
+
* Use this in the `sandboxRunner` config option.
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* ```ts
|
|
273
|
+
* sandboxRunner: sandbox()
|
|
274
|
+
* ```
|
|
275
|
+
*/
|
|
276
|
+
export function sandbox(): string {
|
|
277
|
+
return "@emdash-cms/cloudflare/sandbox";
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Re-export media providers (config-time)
|
|
281
|
+
export { cloudflareImages, type CloudflareImagesConfig } from "./media/images.js";
|
|
282
|
+
export { cloudflareStream, type CloudflareStreamConfig } from "./media/stream.js";
|
|
283
|
+
|
|
284
|
+
// Re-export cache provider config helper (config-time)
|
|
285
|
+
export { cloudflareCache, type CloudflareCacheConfig } from "./cache/config.js";
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare Images 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 { CloudflareImagesConfig } from "./images.js";
|
|
19
|
+
|
|
20
|
+
/** Safely extract a number from an unknown value */
|
|
21
|
+
function toNumber(value: unknown): number | undefined {
|
|
22
|
+
return typeof value === "number" ? value : undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolve a config value, checking env var if direct value not provided
|
|
27
|
+
*/
|
|
28
|
+
function resolveEnvValue(
|
|
29
|
+
directValue: string | undefined,
|
|
30
|
+
envVarName: string | undefined,
|
|
31
|
+
defaultEnvVar: string,
|
|
32
|
+
serviceName: string,
|
|
33
|
+
): string {
|
|
34
|
+
if (directValue) return directValue;
|
|
35
|
+
const envVar = envVarName || defaultEnvVar;
|
|
36
|
+
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Worker binding accessed from untyped env object
|
|
37
|
+
const value = (env as Record<string, string | undefined>)[envVar];
|
|
38
|
+
if (!value) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`${serviceName}: Missing ${envVar}. Set it as an environment variable or provide it directly in config.`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Runtime implementation for Cloudflare Images provider
|
|
48
|
+
*/
|
|
49
|
+
export const createMediaProvider: CreateMediaProviderFn<CloudflareImagesConfig> = (config) => {
|
|
50
|
+
const { deliveryDomain, defaultVariant = "public" } = config;
|
|
51
|
+
|
|
52
|
+
// Lazy getters - resolve env vars at request time, not module init time
|
|
53
|
+
const getAccountId = () =>
|
|
54
|
+
resolveEnvValue(config.accountId, config.accountIdEnvVar, "CF_ACCOUNT_ID", "Cloudflare Images");
|
|
55
|
+
const getAccountHash = () =>
|
|
56
|
+
resolveEnvValue(
|
|
57
|
+
config.accountHash,
|
|
58
|
+
config.accountHashEnvVar,
|
|
59
|
+
"CF_IMAGES_ACCOUNT_HASH",
|
|
60
|
+
"Cloudflare Images",
|
|
61
|
+
);
|
|
62
|
+
const getApiToken = () =>
|
|
63
|
+
resolveEnvValue(config.apiToken, config.apiTokenEnvVar, "CF_IMAGES_TOKEN", "Cloudflare Images");
|
|
64
|
+
const getApiBase = () =>
|
|
65
|
+
`https://api.cloudflare.com/client/v4/accounts/${getAccountId()}/images/v1`;
|
|
66
|
+
const getHeaders = () => ({ Authorization: `Bearer ${getApiToken()}` });
|
|
67
|
+
const getDeliveryBase = () =>
|
|
68
|
+
deliveryDomain ? `https://${deliveryDomain}` : "https://imagedelivery.net";
|
|
69
|
+
|
|
70
|
+
// Build a delivery URL with flexible variant transforms
|
|
71
|
+
const buildUrl = (imageId: string, transforms?: { w?: number; h?: number; fit?: string }) => {
|
|
72
|
+
const base = `${getDeliveryBase()}/${getAccountHash()}/${imageId}`;
|
|
73
|
+
if (!transforms || Object.keys(transforms).length === 0) {
|
|
74
|
+
return `${base}/${defaultVariant}`;
|
|
75
|
+
}
|
|
76
|
+
const parts: string[] = [];
|
|
77
|
+
if (transforms.w) parts.push(`w=${transforms.w}`);
|
|
78
|
+
if (transforms.h) parts.push(`h=${transforms.h}`);
|
|
79
|
+
if (transforms.fit) parts.push(`fit=${transforms.fit}`);
|
|
80
|
+
return `${base}/${parts.join(",")}`;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Fetch image dimensions via the format=json delivery endpoint
|
|
84
|
+
// This is a public endpoint that doesn't require authentication
|
|
85
|
+
const fetchDimensions = async (
|
|
86
|
+
imageId: string,
|
|
87
|
+
): Promise<{ width: number; height: number } | null> => {
|
|
88
|
+
const url = `${getDeliveryBase()}/${getAccountHash()}/${imageId}/format=json`;
|
|
89
|
+
try {
|
|
90
|
+
const response = await fetch(url);
|
|
91
|
+
if (!response.ok) return null;
|
|
92
|
+
const data: ImageJsonResponse = await response.json();
|
|
93
|
+
return { width: data.width, height: data.height };
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const provider: MediaProvider = {
|
|
100
|
+
async list(options: MediaListOptions) {
|
|
101
|
+
const apiBase = getApiBase();
|
|
102
|
+
const headers = getHeaders();
|
|
103
|
+
|
|
104
|
+
const params = new URLSearchParams();
|
|
105
|
+
if (options.cursor) {
|
|
106
|
+
params.set("continuation_token", options.cursor);
|
|
107
|
+
}
|
|
108
|
+
if (options.limit) {
|
|
109
|
+
params.set("per_page", String(options.limit));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const url = `${apiBase}?${params}`;
|
|
113
|
+
const response = await fetch(url, { headers });
|
|
114
|
+
|
|
115
|
+
if (!response.ok) {
|
|
116
|
+
throw new Error(`Cloudflare Images API error: ${response.status}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const data: CloudflareImagesListResponse = await response.json();
|
|
120
|
+
|
|
121
|
+
if (!data.success) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`Cloudflare Images API error: ${data.errors?.[0]?.message || "Unknown error"}`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Filter out images that require signed URLs (not supported yet)
|
|
128
|
+
const publicImages = data.result.images.filter((img) => !img.requireSignedURLs);
|
|
129
|
+
|
|
130
|
+
// Fetch dimensions for all images in parallel
|
|
131
|
+
const dimensionsMap = new Map<string, { width: number; height: number }>();
|
|
132
|
+
const dimensionResults = await Promise.all(
|
|
133
|
+
publicImages.map(async (img) => {
|
|
134
|
+
const dims = await fetchDimensions(img.id);
|
|
135
|
+
return { id: img.id, dims };
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
for (const { id, dims } of dimensionResults) {
|
|
139
|
+
if (dims) dimensionsMap.set(id, dims);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
items: publicImages.map((img) => {
|
|
144
|
+
const dims = dimensionsMap.get(img.id);
|
|
145
|
+
return {
|
|
146
|
+
id: img.id,
|
|
147
|
+
filename: img.filename || img.id,
|
|
148
|
+
mimeType: "image/jpeg", // CF Images doesn't expose original mime type
|
|
149
|
+
width: dims?.width ?? toNumber(img.meta?.width),
|
|
150
|
+
height: dims?.height ?? toNumber(img.meta?.height),
|
|
151
|
+
// Use 400px wide preview for grid thumbnails (good for 2x retina on ~200px grid)
|
|
152
|
+
previewUrl: buildUrl(img.id, { w: 400, fit: "scale-down" }),
|
|
153
|
+
meta: {
|
|
154
|
+
variants: img.variants,
|
|
155
|
+
uploaded: img.uploaded,
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}),
|
|
159
|
+
nextCursor: data.result.continuation_token || undefined,
|
|
160
|
+
};
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
async get(id: string) {
|
|
164
|
+
const apiBase = getApiBase();
|
|
165
|
+
const headers = getHeaders();
|
|
166
|
+
|
|
167
|
+
const url = `${apiBase}/${id}`;
|
|
168
|
+
const response = await fetch(url, { headers });
|
|
169
|
+
|
|
170
|
+
if (!response.ok) {
|
|
171
|
+
if (response.status === 404) return null;
|
|
172
|
+
throw new Error(`Cloudflare Images API error: ${response.status}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const data: CloudflareImageResponse = await response.json();
|
|
176
|
+
|
|
177
|
+
if (!data.success) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const img = data.result;
|
|
182
|
+
|
|
183
|
+
// Don't return images that require signed URLs (not supported yet)
|
|
184
|
+
if (img.requireSignedURLs) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Fetch dimensions via format=json endpoint
|
|
189
|
+
const dims = await fetchDimensions(img.id);
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
id: img.id,
|
|
193
|
+
filename: img.filename || img.id,
|
|
194
|
+
mimeType: "image/jpeg",
|
|
195
|
+
width: dims?.width ?? toNumber(img.meta?.width),
|
|
196
|
+
height: dims?.height ?? toNumber(img.meta?.height),
|
|
197
|
+
// Use larger preview for detail view
|
|
198
|
+
previewUrl: buildUrl(img.id, { w: 800, fit: "scale-down" }),
|
|
199
|
+
meta: {
|
|
200
|
+
variants: img.variants,
|
|
201
|
+
uploaded: img.uploaded,
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
async upload(input) {
|
|
207
|
+
const apiBase = getApiBase();
|
|
208
|
+
const apiToken = getApiToken();
|
|
209
|
+
|
|
210
|
+
const formData = new FormData();
|
|
211
|
+
formData.append("file", input.file, input.filename);
|
|
212
|
+
|
|
213
|
+
// Ensure uploaded images are public (don't require signed URLs)
|
|
214
|
+
formData.append("requireSignedURLs", "false");
|
|
215
|
+
|
|
216
|
+
// Add metadata if provided
|
|
217
|
+
const metadata: Record<string, string> = {};
|
|
218
|
+
if (input.alt) {
|
|
219
|
+
metadata.alt = input.alt;
|
|
220
|
+
}
|
|
221
|
+
if (Object.keys(metadata).length > 0) {
|
|
222
|
+
formData.append("metadata", JSON.stringify(metadata));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const response = await fetch(apiBase, {
|
|
226
|
+
method: "POST",
|
|
227
|
+
headers: {
|
|
228
|
+
Authorization: `Bearer ${apiToken}`,
|
|
229
|
+
// Don't set Content-Type - let browser set it with boundary
|
|
230
|
+
},
|
|
231
|
+
body: formData,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
if (!response.ok) {
|
|
235
|
+
const error = await response.text();
|
|
236
|
+
throw new Error(`Cloudflare Images upload failed: ${error}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const data: CloudflareImageResponse = await response.json();
|
|
240
|
+
|
|
241
|
+
if (!data.success) {
|
|
242
|
+
throw new Error(
|
|
243
|
+
`Cloudflare Images upload failed: ${data.errors?.[0]?.message || "Unknown error"}`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const img = data.result;
|
|
248
|
+
return {
|
|
249
|
+
id: img.id,
|
|
250
|
+
filename: img.filename || input.filename,
|
|
251
|
+
mimeType: "image/jpeg",
|
|
252
|
+
width: toNumber(img.meta?.width),
|
|
253
|
+
height: toNumber(img.meta?.height),
|
|
254
|
+
previewUrl: buildUrl(img.id, { w: 400, fit: "scale-down" }),
|
|
255
|
+
meta: {
|
|
256
|
+
variants: img.variants,
|
|
257
|
+
uploaded: img.uploaded,
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
async delete(id: string) {
|
|
263
|
+
const apiBase = getApiBase();
|
|
264
|
+
const headers = getHeaders();
|
|
265
|
+
|
|
266
|
+
const response = await fetch(`${apiBase}/${id}`, {
|
|
267
|
+
method: "DELETE",
|
|
268
|
+
headers,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
if (!response.ok && response.status !== 404) {
|
|
272
|
+
throw new Error(`Cloudflare Images delete failed: ${response.status}`);
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
getEmbed(value: MediaValue, options?: EmbedOptions): EmbedResult {
|
|
277
|
+
const accountHash = getAccountHash();
|
|
278
|
+
const deliveryBase = getDeliveryBase();
|
|
279
|
+
const baseUrl = `${deliveryBase}/${accountHash}/${value.id}`;
|
|
280
|
+
|
|
281
|
+
// Helper to build URL with transforms
|
|
282
|
+
const buildSrc = (opts: { width?: number; height?: number; format?: string }) => {
|
|
283
|
+
const t: string[] = [];
|
|
284
|
+
if (opts.width) t.push(`w=${opts.width}`);
|
|
285
|
+
if (opts.height) t.push(`h=${opts.height}`);
|
|
286
|
+
if (opts.format) t.push(`f=${opts.format}`);
|
|
287
|
+
t.push("fit=scale-down");
|
|
288
|
+
return `${baseUrl}/${t.join(",")}`;
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
// Build src URL - always include transforms (CF Images requires a variant)
|
|
292
|
+
const width = options?.width ?? value.width ?? 1200;
|
|
293
|
+
const height = options?.height ?? value.height;
|
|
294
|
+
const src = buildSrc({ width, height, format: options?.format });
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
type: "image",
|
|
298
|
+
src,
|
|
299
|
+
width: options?.width ?? value.width,
|
|
300
|
+
height: options?.height ?? value.height,
|
|
301
|
+
alt: value.alt,
|
|
302
|
+
// Provide getSrc for dynamic resizing (e.g., responsive images)
|
|
303
|
+
getSrc: buildSrc,
|
|
304
|
+
};
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
getThumbnailUrl(id: string, _mimeType?: string, options?: { width?: number; height?: number }) {
|
|
308
|
+
// For images, return a sized delivery URL
|
|
309
|
+
const width = options?.width || 400;
|
|
310
|
+
const height = options?.height;
|
|
311
|
+
return buildUrl(id, { w: width, h: height, fit: "scale-down" });
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
return provider;
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// Cloudflare API response types
|
|
319
|
+
interface CloudflareImagesListResponse {
|
|
320
|
+
success: boolean;
|
|
321
|
+
errors?: Array<{ message: string }>;
|
|
322
|
+
result: {
|
|
323
|
+
images: CloudflareImage[];
|
|
324
|
+
continuation_token?: string;
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
interface CloudflareImageResponse {
|
|
329
|
+
success: boolean;
|
|
330
|
+
errors?: Array<{ message: string }>;
|
|
331
|
+
result: CloudflareImage;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
interface CloudflareImage {
|
|
335
|
+
id: string;
|
|
336
|
+
filename?: string;
|
|
337
|
+
uploaded: string;
|
|
338
|
+
requireSignedURLs: boolean;
|
|
339
|
+
variants: string[];
|
|
340
|
+
meta?: Record<string, unknown>;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Response from format=json delivery endpoint
|
|
344
|
+
interface ImageJsonResponse {
|
|
345
|
+
width: number;
|
|
346
|
+
height: number;
|
|
347
|
+
original: {
|
|
348
|
+
file_size: number;
|
|
349
|
+
width: number;
|
|
350
|
+
height: number;
|
|
351
|
+
format: string;
|
|
352
|
+
};
|
|
353
|
+
}
|