@cloudwerk/images 0.1.3
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/index.d.ts +1209 -0
- package/dist/index.js +823 -0
- package/dist/index.js.map +1 -0
- package/package.json +36 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,823 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var ImageError = class extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "ImageError";
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
var ImageConfigError = class extends ImageError {
|
|
9
|
+
field;
|
|
10
|
+
constructor(message, field) {
|
|
11
|
+
super(`Image configuration error in '${field}': ${message}`);
|
|
12
|
+
this.name = "ImageConfigError";
|
|
13
|
+
this.field = field;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
var ImageEnvError = class extends ImageError {
|
|
17
|
+
envVar;
|
|
18
|
+
constructor(envVar) {
|
|
19
|
+
super(
|
|
20
|
+
`Missing required environment variable '${envVar}' for Cloudflare Images`
|
|
21
|
+
);
|
|
22
|
+
this.name = "ImageEnvError";
|
|
23
|
+
this.envVar = envVar;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
var ImageNotFoundError = class extends ImageError {
|
|
27
|
+
imageId;
|
|
28
|
+
constructor(imageId) {
|
|
29
|
+
super(`Image '${imageId}' not found`);
|
|
30
|
+
this.name = "ImageNotFoundError";
|
|
31
|
+
this.imageId = imageId;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
var ImageUploadError = class extends ImageError {
|
|
35
|
+
statusCode;
|
|
36
|
+
details;
|
|
37
|
+
constructor(message, statusCode, details) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.name = "ImageUploadError";
|
|
40
|
+
this.statusCode = statusCode;
|
|
41
|
+
this.details = details;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
var ImageApiError = class extends ImageError {
|
|
45
|
+
statusCode;
|
|
46
|
+
errors;
|
|
47
|
+
constructor(statusCode, errors) {
|
|
48
|
+
const messages = errors.map((e) => e.message).join(", ");
|
|
49
|
+
super(`Cloudflare Images API error (${statusCode}): ${messages}`);
|
|
50
|
+
this.name = "ImageApiError";
|
|
51
|
+
this.statusCode = statusCode;
|
|
52
|
+
this.errors = errors;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
var ImageVariantError = class extends ImageError {
|
|
56
|
+
variant;
|
|
57
|
+
availableVariants;
|
|
58
|
+
constructor(variant, availableVariants) {
|
|
59
|
+
const available = availableVariants.length > 0 ? `Available variants: ${availableVariants.join(", ")}` : "No variants are configured";
|
|
60
|
+
super(`Invalid variant '${variant}'. ${available}`);
|
|
61
|
+
this.name = "ImageVariantError";
|
|
62
|
+
this.variant = variant;
|
|
63
|
+
this.availableVariants = availableVariants;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
var ImageContextError = class extends ImageError {
|
|
67
|
+
constructor() {
|
|
68
|
+
super(`Image accessed outside of request handler.
|
|
69
|
+
|
|
70
|
+
This can happen when:
|
|
71
|
+
1. Accessing images at module-load time (top-level code)
|
|
72
|
+
2. Accessing images in a setTimeout/setInterval callback
|
|
73
|
+
3. The request context was not properly initialized
|
|
74
|
+
|
|
75
|
+
Images can only be accessed during request handling within a Cloudwerk application.
|
|
76
|
+
|
|
77
|
+
Example of correct usage:
|
|
78
|
+
import { images } from '@cloudwerk/core/bindings'
|
|
79
|
+
|
|
80
|
+
export async function POST(request: Request) {
|
|
81
|
+
const formData = await request.formData()
|
|
82
|
+
const file = formData.get('image') as File
|
|
83
|
+
const result = await images.avatars.upload(file)
|
|
84
|
+
return json(result)
|
|
85
|
+
}
|
|
86
|
+
`);
|
|
87
|
+
this.name = "ImageContextError";
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
var ImageBindingNotFoundError = class extends ImageError {
|
|
91
|
+
name;
|
|
92
|
+
availableImages;
|
|
93
|
+
constructor(name, availableImages) {
|
|
94
|
+
const available = availableImages.length > 0 ? `Available images: ${availableImages.join(", ")}` : "No images are configured";
|
|
95
|
+
super(`Image '${name}' not found in current environment.
|
|
96
|
+
|
|
97
|
+
${available}
|
|
98
|
+
|
|
99
|
+
To add this image, create a file at app/images/${name}.ts with:
|
|
100
|
+
import { defineImage } from '@cloudwerk/images'
|
|
101
|
+
export default defineImage({
|
|
102
|
+
variants: {
|
|
103
|
+
thumbnail: { width: 100, height: 100, fit: 'cover' },
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
`);
|
|
107
|
+
this.name = "ImageBindingNotFoundError";
|
|
108
|
+
this.availableImages = availableImages;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// src/define-image.ts
|
|
113
|
+
function validateVariant(variant, name) {
|
|
114
|
+
if (variant.width !== void 0) {
|
|
115
|
+
if (!Number.isInteger(variant.width) || variant.width < 1) {
|
|
116
|
+
throw new ImageConfigError(
|
|
117
|
+
`width must be a positive integer`,
|
|
118
|
+
`variants.${name}.width`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (variant.height !== void 0) {
|
|
123
|
+
if (!Number.isInteger(variant.height) || variant.height < 1) {
|
|
124
|
+
throw new ImageConfigError(
|
|
125
|
+
`height must be a positive integer`,
|
|
126
|
+
`variants.${name}.height`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (variant.blur !== void 0) {
|
|
131
|
+
if (variant.blur < 1 || variant.blur > 250) {
|
|
132
|
+
throw new ImageConfigError(
|
|
133
|
+
`blur must be between 1 and 250`,
|
|
134
|
+
`variants.${name}.blur`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (variant.quality !== void 0) {
|
|
139
|
+
if (variant.quality < 1 || variant.quality > 100) {
|
|
140
|
+
throw new ImageConfigError(
|
|
141
|
+
`quality must be between 1 and 100`,
|
|
142
|
+
`variants.${name}.quality`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (variant.dpr !== void 0) {
|
|
147
|
+
if (variant.dpr < 1 || variant.dpr > 3) {
|
|
148
|
+
throw new ImageConfigError(
|
|
149
|
+
`dpr must be between 1 and 3`,
|
|
150
|
+
`variants.${name}.dpr`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (variant.sharpen !== void 0) {
|
|
155
|
+
if (variant.sharpen < 0 || variant.sharpen > 10) {
|
|
156
|
+
throw new ImageConfigError(
|
|
157
|
+
`sharpen must be between 0 and 10`,
|
|
158
|
+
`variants.${name}.sharpen`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (variant.brightness !== void 0) {
|
|
163
|
+
if (variant.brightness < -1 || variant.brightness > 1) {
|
|
164
|
+
throw new ImageConfigError(
|
|
165
|
+
`brightness must be between -1 and 1`,
|
|
166
|
+
`variants.${name}.brightness`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (variant.contrast !== void 0) {
|
|
171
|
+
if (variant.contrast < -1 || variant.contrast > 1) {
|
|
172
|
+
throw new ImageConfigError(
|
|
173
|
+
`contrast must be between -1 and 1`,
|
|
174
|
+
`variants.${name}.contrast`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (variant.rotate !== void 0) {
|
|
179
|
+
if (variant.rotate < 0 || variant.rotate > 360) {
|
|
180
|
+
throw new ImageConfigError(
|
|
181
|
+
`rotate must be between 0 and 360`,
|
|
182
|
+
`variants.${name}.rotate`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const validFits = ["cover", "contain", "scale-down", "crop", "pad"];
|
|
187
|
+
if (variant.fit !== void 0 && !validFits.includes(variant.fit)) {
|
|
188
|
+
throw new ImageConfigError(
|
|
189
|
+
`fit must be one of: ${validFits.join(", ")}`,
|
|
190
|
+
`variants.${name}.fit`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
const validFormats = ["webp", "avif", "json", "jpeg", "png"];
|
|
194
|
+
if (variant.format !== void 0 && !validFormats.includes(variant.format)) {
|
|
195
|
+
throw new ImageConfigError(
|
|
196
|
+
`format must be one of: ${validFormats.join(", ")}`,
|
|
197
|
+
`variants.${name}.format`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
const validGravities = ["auto", "center", "top", "bottom", "left", "right", "face"];
|
|
201
|
+
if (variant.gravity !== void 0 && !validGravities.includes(variant.gravity)) {
|
|
202
|
+
throw new ImageConfigError(
|
|
203
|
+
`gravity must be one of: ${validGravities.join(", ")}`,
|
|
204
|
+
`variants.${name}.gravity`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
const validMetadatas = ["keep", "copyright", "none"];
|
|
208
|
+
if (variant.metadata !== void 0 && !validMetadatas.includes(variant.metadata)) {
|
|
209
|
+
throw new ImageConfigError(
|
|
210
|
+
`metadata must be one of: ${validMetadatas.join(", ")}`,
|
|
211
|
+
`variants.${name}.metadata`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function validateConfig(config) {
|
|
216
|
+
if (config.name !== void 0) {
|
|
217
|
+
if (typeof config.name !== "string" || config.name.length === 0) {
|
|
218
|
+
throw new ImageConfigError("name must be a non-empty string", "name");
|
|
219
|
+
}
|
|
220
|
+
if (!/^[a-z][a-z0-9-]*$/.test(config.name)) {
|
|
221
|
+
throw new ImageConfigError(
|
|
222
|
+
"name must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens",
|
|
223
|
+
"name"
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (config.deliveryUrl !== void 0) {
|
|
228
|
+
if (typeof config.deliveryUrl !== "string") {
|
|
229
|
+
throw new ImageConfigError(
|
|
230
|
+
"deliveryUrl must be a string",
|
|
231
|
+
"deliveryUrl"
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
new URL(config.deliveryUrl);
|
|
236
|
+
} catch {
|
|
237
|
+
throw new ImageConfigError(
|
|
238
|
+
"deliveryUrl must be a valid URL",
|
|
239
|
+
"deliveryUrl"
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (config.variants !== void 0) {
|
|
244
|
+
if (typeof config.variants !== "object" || config.variants === null) {
|
|
245
|
+
throw new ImageConfigError("variants must be an object", "variants");
|
|
246
|
+
}
|
|
247
|
+
for (const [name, variant] of Object.entries(config.variants)) {
|
|
248
|
+
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
249
|
+
throw new ImageConfigError(
|
|
250
|
+
`variant name '${name}' must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens`,
|
|
251
|
+
"variants"
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
validateVariant(variant, name);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (config.defaultVariant !== void 0 && config.variants !== void 0) {
|
|
258
|
+
if (!(config.defaultVariant in config.variants)) {
|
|
259
|
+
throw new ImageConfigError(
|
|
260
|
+
`defaultVariant '${config.defaultVariant}' is not defined in variants`,
|
|
261
|
+
"defaultVariant"
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function defineImage(config = {}) {
|
|
267
|
+
validateConfig(config);
|
|
268
|
+
const definition = {
|
|
269
|
+
__brand: "cloudwerk-image",
|
|
270
|
+
name: config.name ?? "",
|
|
271
|
+
// Will be set from filename if empty
|
|
272
|
+
config,
|
|
273
|
+
variants: config.variants ?? {}
|
|
274
|
+
};
|
|
275
|
+
return definition;
|
|
276
|
+
}
|
|
277
|
+
function isImageDefinition(value) {
|
|
278
|
+
return typeof value === "object" && value !== null && "__brand" in value && value.__brand === "cloudwerk-image";
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// src/cloudflare-types.ts
|
|
282
|
+
var CLOUDFLARE_IMAGES_API = {
|
|
283
|
+
/** Base URL for Cloudflare API v4 */
|
|
284
|
+
BASE: "https://api.cloudflare.com/client/v4",
|
|
285
|
+
/**
|
|
286
|
+
* Get the images endpoint for an account.
|
|
287
|
+
*/
|
|
288
|
+
images: (accountId) => `https://api.cloudflare.com/client/v4/accounts/${accountId}/images/v1`,
|
|
289
|
+
/**
|
|
290
|
+
* Get the direct upload endpoint.
|
|
291
|
+
*/
|
|
292
|
+
directUpload: (accountId) => `https://api.cloudflare.com/client/v4/accounts/${accountId}/images/v2/direct_upload`,
|
|
293
|
+
/**
|
|
294
|
+
* Get a specific image endpoint.
|
|
295
|
+
*/
|
|
296
|
+
image: (accountId, imageId) => `https://api.cloudflare.com/client/v4/accounts/${accountId}/images/v1/${imageId}`,
|
|
297
|
+
/**
|
|
298
|
+
* Get the default delivery URL format.
|
|
299
|
+
*/
|
|
300
|
+
deliveryUrl: (accountId, imageId, variant) => `https://imagedelivery.net/${accountId}/${imageId}/${variant}`
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
// src/client.ts
|
|
304
|
+
function parseExpiry(duration) {
|
|
305
|
+
let seconds;
|
|
306
|
+
if (typeof duration === "number") {
|
|
307
|
+
seconds = duration;
|
|
308
|
+
} else {
|
|
309
|
+
const match = duration.match(/^(\d+)(s|m|h|d)$/);
|
|
310
|
+
if (!match) {
|
|
311
|
+
seconds = 30 * 60;
|
|
312
|
+
} else {
|
|
313
|
+
const value = parseInt(match[1], 10);
|
|
314
|
+
const unit = match[2];
|
|
315
|
+
switch (unit) {
|
|
316
|
+
case "s":
|
|
317
|
+
seconds = value;
|
|
318
|
+
break;
|
|
319
|
+
case "m":
|
|
320
|
+
seconds = value * 60;
|
|
321
|
+
break;
|
|
322
|
+
case "h":
|
|
323
|
+
seconds = value * 3600;
|
|
324
|
+
break;
|
|
325
|
+
case "d":
|
|
326
|
+
seconds = value * 86400;
|
|
327
|
+
break;
|
|
328
|
+
default:
|
|
329
|
+
seconds = 30 * 60;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const expiryDate = new Date(Date.now() + seconds * 1e3);
|
|
334
|
+
return expiryDate.toISOString();
|
|
335
|
+
}
|
|
336
|
+
function toImageResult(image) {
|
|
337
|
+
return {
|
|
338
|
+
id: image.id,
|
|
339
|
+
filename: image.filename || void 0,
|
|
340
|
+
uploaded: new Date(image.uploaded),
|
|
341
|
+
variants: image.variants.map((url) => {
|
|
342
|
+
const parts = url.split("/");
|
|
343
|
+
return parts[parts.length - 1];
|
|
344
|
+
}),
|
|
345
|
+
metadata: image.meta,
|
|
346
|
+
requireSignedURLs: image.requireSignedURLs
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
var ImageClient = class {
|
|
350
|
+
accountId;
|
|
351
|
+
apiToken;
|
|
352
|
+
deliveryUrl;
|
|
353
|
+
variants;
|
|
354
|
+
variantNames;
|
|
355
|
+
constructor(accountId, apiToken, variants = {}, deliveryUrl) {
|
|
356
|
+
this.accountId = accountId;
|
|
357
|
+
this.apiToken = apiToken;
|
|
358
|
+
this.variants = variants;
|
|
359
|
+
this.variantNames = Object.keys(variants);
|
|
360
|
+
this.deliveryUrl = deliveryUrl;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Make an authenticated API request.
|
|
364
|
+
*/
|
|
365
|
+
async fetch(endpoint, options = {}) {
|
|
366
|
+
const response = await fetch(endpoint, {
|
|
367
|
+
...options,
|
|
368
|
+
headers: {
|
|
369
|
+
Authorization: `Bearer ${this.apiToken}`,
|
|
370
|
+
...options.headers
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
const data = await response.json();
|
|
374
|
+
if (!response.ok || !data.success) {
|
|
375
|
+
throw new ImageApiError(response.status, data.errors || []);
|
|
376
|
+
}
|
|
377
|
+
return data;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Upload an image file.
|
|
381
|
+
*/
|
|
382
|
+
async upload(file, options = {}) {
|
|
383
|
+
const endpoint = CLOUDFLARE_IMAGES_API.images(this.accountId);
|
|
384
|
+
const formData = new FormData();
|
|
385
|
+
if (file instanceof ReadableStream) {
|
|
386
|
+
const response = new Response(file);
|
|
387
|
+
const blob = await response.blob();
|
|
388
|
+
formData.append("file", blob);
|
|
389
|
+
} else {
|
|
390
|
+
formData.append("file", file);
|
|
391
|
+
}
|
|
392
|
+
if (options.id) {
|
|
393
|
+
formData.append("id", options.id);
|
|
394
|
+
}
|
|
395
|
+
if (options.requireSignedURLs !== void 0) {
|
|
396
|
+
formData.append("requireSignedURLs", String(options.requireSignedURLs));
|
|
397
|
+
}
|
|
398
|
+
if (options.metadata) {
|
|
399
|
+
formData.append("metadata", JSON.stringify(options.metadata));
|
|
400
|
+
}
|
|
401
|
+
try {
|
|
402
|
+
const response = await this.fetch(endpoint, {
|
|
403
|
+
method: "POST",
|
|
404
|
+
body: formData
|
|
405
|
+
});
|
|
406
|
+
return toImageResult(response.result);
|
|
407
|
+
} catch (error) {
|
|
408
|
+
if (error instanceof ImageApiError) {
|
|
409
|
+
throw new ImageUploadError(
|
|
410
|
+
`Failed to upload image: ${error.message}`,
|
|
411
|
+
error.statusCode,
|
|
412
|
+
error.errors.map((e) => e.message).join(", ")
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
throw error;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Generate a direct upload URL for client-side uploads.
|
|
420
|
+
*/
|
|
421
|
+
async getDirectUploadUrl(options = {}) {
|
|
422
|
+
const endpoint = CLOUDFLARE_IMAGES_API.directUpload(this.accountId);
|
|
423
|
+
const body = {};
|
|
424
|
+
if (options.requireSignedURLs !== void 0) {
|
|
425
|
+
body.requireSignedURLs = options.requireSignedURLs;
|
|
426
|
+
}
|
|
427
|
+
if (options.metadata) {
|
|
428
|
+
body.metadata = options.metadata;
|
|
429
|
+
}
|
|
430
|
+
if (options.expiry) {
|
|
431
|
+
body.expiry = parseExpiry(options.expiry);
|
|
432
|
+
}
|
|
433
|
+
const response = await this.fetch(
|
|
434
|
+
endpoint,
|
|
435
|
+
{
|
|
436
|
+
method: "POST",
|
|
437
|
+
headers: {
|
|
438
|
+
"Content-Type": "application/json"
|
|
439
|
+
},
|
|
440
|
+
body: JSON.stringify(body)
|
|
441
|
+
}
|
|
442
|
+
);
|
|
443
|
+
return {
|
|
444
|
+
uploadUrl: response.result.uploadURL,
|
|
445
|
+
id: response.result.id
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Delete an image.
|
|
450
|
+
*/
|
|
451
|
+
async delete(imageId) {
|
|
452
|
+
const endpoint = CLOUDFLARE_IMAGES_API.image(this.accountId, imageId);
|
|
453
|
+
try {
|
|
454
|
+
await this.fetch(endpoint, {
|
|
455
|
+
method: "DELETE"
|
|
456
|
+
});
|
|
457
|
+
} catch (error) {
|
|
458
|
+
if (error instanceof ImageApiError && error.statusCode === 404) {
|
|
459
|
+
throw new ImageNotFoundError(imageId);
|
|
460
|
+
}
|
|
461
|
+
throw error;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Get image details.
|
|
466
|
+
*/
|
|
467
|
+
async get(imageId) {
|
|
468
|
+
const endpoint = CLOUDFLARE_IMAGES_API.image(this.accountId, imageId);
|
|
469
|
+
try {
|
|
470
|
+
const response = await this.fetch(endpoint);
|
|
471
|
+
return toImageResult(response.result);
|
|
472
|
+
} catch (error) {
|
|
473
|
+
if (error instanceof ImageApiError && error.statusCode === 404) {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
throw error;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* List images with pagination.
|
|
481
|
+
*/
|
|
482
|
+
async list(options = {}) {
|
|
483
|
+
const endpoint = CLOUDFLARE_IMAGES_API.images(this.accountId);
|
|
484
|
+
const params = new URLSearchParams();
|
|
485
|
+
if (options.page !== void 0) {
|
|
486
|
+
params.set("page", String(options.page));
|
|
487
|
+
}
|
|
488
|
+
if (options.perPage !== void 0) {
|
|
489
|
+
params.set("per_page", String(Math.min(100, Math.max(1, options.perPage))));
|
|
490
|
+
}
|
|
491
|
+
const urlWithParams = params.toString() ? `${endpoint}?${params}` : endpoint;
|
|
492
|
+
const response = await this.fetch(urlWithParams);
|
|
493
|
+
return response.result.images.map(toImageResult);
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Generate URL for an image with optional variant.
|
|
497
|
+
*/
|
|
498
|
+
url(imageId, variant) {
|
|
499
|
+
if (variant !== void 0 && !this.variantNames.includes(variant)) {
|
|
500
|
+
throw new ImageVariantError(variant, this.variantNames);
|
|
501
|
+
}
|
|
502
|
+
if (this.deliveryUrl) {
|
|
503
|
+
const variantPath = variant ? `/${variant}` : "";
|
|
504
|
+
return `${this.deliveryUrl}/${imageId}${variantPath}`;
|
|
505
|
+
}
|
|
506
|
+
const variantName = variant ?? "public";
|
|
507
|
+
return CLOUDFLARE_IMAGES_API.deliveryUrl(
|
|
508
|
+
this.accountId,
|
|
509
|
+
imageId,
|
|
510
|
+
variantName
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
function createImageClient(accountId, apiToken, variants = {}, deliveryUrl) {
|
|
515
|
+
return new ImageClient(accountId, apiToken, variants, deliveryUrl);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// src/transformer.ts
|
|
519
|
+
var ImageTransformError = class extends Error {
|
|
520
|
+
status;
|
|
521
|
+
code;
|
|
522
|
+
constructor(message, status, code) {
|
|
523
|
+
super(message);
|
|
524
|
+
this.name = "ImageTransformError";
|
|
525
|
+
this.status = status;
|
|
526
|
+
this.code = code;
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
function parseTransformParams(searchParams, config) {
|
|
530
|
+
const options = {};
|
|
531
|
+
const presetName = searchParams.get("preset");
|
|
532
|
+
if (presetName && config.presets?.[presetName]) {
|
|
533
|
+
Object.assign(options, config.presets[presetName]);
|
|
534
|
+
}
|
|
535
|
+
if (config.defaults) {
|
|
536
|
+
Object.assign(options, config.defaults, options);
|
|
537
|
+
}
|
|
538
|
+
if (config.allowArbitrary !== false) {
|
|
539
|
+
const width = searchParams.get("w") || searchParams.get("width");
|
|
540
|
+
if (width) {
|
|
541
|
+
const w = parseInt(width, 10);
|
|
542
|
+
if (!isNaN(w) && w > 0) {
|
|
543
|
+
options.width = Math.min(w, config.maxWidth ?? 4096);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
const height = searchParams.get("h") || searchParams.get("height");
|
|
547
|
+
if (height) {
|
|
548
|
+
const h = parseInt(height, 10);
|
|
549
|
+
if (!isNaN(h) && h > 0) {
|
|
550
|
+
options.height = Math.min(h, config.maxHeight ?? 4096);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
const fit = searchParams.get("fit");
|
|
554
|
+
if (fit && ["cover", "contain", "scale-down", "crop", "pad"].includes(fit)) {
|
|
555
|
+
options.fit = fit;
|
|
556
|
+
}
|
|
557
|
+
const format = searchParams.get("format") || searchParams.get("f");
|
|
558
|
+
if (format && ["webp", "avif", "jpeg", "png", "auto"].includes(format)) {
|
|
559
|
+
options.format = format;
|
|
560
|
+
}
|
|
561
|
+
const quality = searchParams.get("q") || searchParams.get("quality");
|
|
562
|
+
if (quality) {
|
|
563
|
+
const q = parseInt(quality, 10);
|
|
564
|
+
if (!isNaN(q) && q >= 1 && q <= 100) {
|
|
565
|
+
options.quality = q;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
const dpr = searchParams.get("dpr");
|
|
569
|
+
if (dpr) {
|
|
570
|
+
const d = parseFloat(dpr);
|
|
571
|
+
if (!isNaN(d) && d >= 1 && d <= 3) {
|
|
572
|
+
options.dpr = d;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
const gravity = searchParams.get("gravity");
|
|
576
|
+
if (gravity && ["auto", "center", "top", "bottom", "left", "right", "face"].includes(gravity)) {
|
|
577
|
+
options.gravity = gravity;
|
|
578
|
+
}
|
|
579
|
+
const blur = searchParams.get("blur");
|
|
580
|
+
if (blur) {
|
|
581
|
+
const b = parseInt(blur, 10);
|
|
582
|
+
if (!isNaN(b) && b >= 1 && b <= 250) {
|
|
583
|
+
options.blur = b;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
const sharpen = searchParams.get("sharpen");
|
|
587
|
+
if (sharpen) {
|
|
588
|
+
const s = parseFloat(sharpen);
|
|
589
|
+
if (!isNaN(s) && s >= 0 && s <= 10) {
|
|
590
|
+
options.sharpen = s;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
const brightness = searchParams.get("brightness");
|
|
594
|
+
if (brightness) {
|
|
595
|
+
const br = parseFloat(brightness);
|
|
596
|
+
if (!isNaN(br) && br >= -1 && br <= 1) {
|
|
597
|
+
options.brightness = br;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
const contrast = searchParams.get("contrast");
|
|
601
|
+
if (contrast) {
|
|
602
|
+
const c = parseFloat(contrast);
|
|
603
|
+
if (!isNaN(c) && c >= -1 && c <= 1) {
|
|
604
|
+
options.contrast = c;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
const rotate = searchParams.get("rotate");
|
|
608
|
+
if (rotate) {
|
|
609
|
+
const r = parseInt(rotate, 10);
|
|
610
|
+
if ([0, 90, 180, 270].includes(r)) {
|
|
611
|
+
options.rotate = r;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return options;
|
|
616
|
+
}
|
|
617
|
+
function presetToCfOptions(preset) {
|
|
618
|
+
const options = {};
|
|
619
|
+
if (preset.width !== void 0) options.width = preset.width;
|
|
620
|
+
if (preset.height !== void 0) options.height = preset.height;
|
|
621
|
+
if (preset.fit !== void 0) options.fit = preset.fit;
|
|
622
|
+
if (preset.format !== void 0) options.format = preset.format;
|
|
623
|
+
if (preset.quality !== void 0) options.quality = preset.quality;
|
|
624
|
+
if (preset.dpr !== void 0) options.dpr = preset.dpr;
|
|
625
|
+
if (preset.gravity !== void 0) options.gravity = preset.gravity;
|
|
626
|
+
if (preset.sharpen !== void 0) options.sharpen = preset.sharpen;
|
|
627
|
+
if (preset.blur !== void 0) options.blur = preset.blur;
|
|
628
|
+
if (preset.brightness !== void 0) options.brightness = preset.brightness;
|
|
629
|
+
if (preset.contrast !== void 0) options.contrast = preset.contrast;
|
|
630
|
+
if (preset.rotate !== void 0) options.rotate = preset.rotate;
|
|
631
|
+
if (preset.metadata !== void 0) options.metadata = preset.metadata;
|
|
632
|
+
if (preset.background !== void 0) options.background = preset.background;
|
|
633
|
+
return options;
|
|
634
|
+
}
|
|
635
|
+
function validateOrigin(sourceUrl, allowedOrigins) {
|
|
636
|
+
if (!allowedOrigins || allowedOrigins.length === 0) {
|
|
637
|
+
return true;
|
|
638
|
+
}
|
|
639
|
+
const sourceOrigin = sourceUrl.origin;
|
|
640
|
+
return allowedOrigins.some((origin) => {
|
|
641
|
+
if (origin === sourceOrigin) return true;
|
|
642
|
+
if (origin.includes("*")) {
|
|
643
|
+
const pattern = origin.replace(/\*/g, "[^.]+");
|
|
644
|
+
const regex = new RegExp(`^${pattern}$`);
|
|
645
|
+
return regex.test(sourceOrigin);
|
|
646
|
+
}
|
|
647
|
+
return false;
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
function createImageTransformer(config = {}) {
|
|
651
|
+
const {
|
|
652
|
+
allowedOrigins,
|
|
653
|
+
presets = {},
|
|
654
|
+
defaults = {},
|
|
655
|
+
maxWidth = 4096,
|
|
656
|
+
maxHeight = 4096,
|
|
657
|
+
cacheControl = "public, max-age=31536000",
|
|
658
|
+
allowArbitrary = true,
|
|
659
|
+
validateSource
|
|
660
|
+
} = config;
|
|
661
|
+
return async (request, context) => {
|
|
662
|
+
const pathParam = context.params.path;
|
|
663
|
+
if (!pathParam) {
|
|
664
|
+
return new Response(JSON.stringify({ error: "Missing image path" }), {
|
|
665
|
+
status: 400,
|
|
666
|
+
headers: { "Content-Type": "application/json" }
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
const sourcePath = Array.isArray(pathParam) ? pathParam.join("/") : pathParam;
|
|
670
|
+
let sourceUrl;
|
|
671
|
+
try {
|
|
672
|
+
sourceUrl = new URL(sourcePath);
|
|
673
|
+
} catch {
|
|
674
|
+
return new Response(JSON.stringify({ error: "Invalid image URL" }), {
|
|
675
|
+
status: 400,
|
|
676
|
+
headers: { "Content-Type": "application/json" }
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
if (!validateOrigin(sourceUrl, allowedOrigins)) {
|
|
680
|
+
return new Response(JSON.stringify({ error: "Origin not allowed" }), {
|
|
681
|
+
status: 403,
|
|
682
|
+
headers: { "Content-Type": "application/json" }
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
if (validateSource) {
|
|
686
|
+
const isValid = await validateSource(sourceUrl);
|
|
687
|
+
if (!isValid) {
|
|
688
|
+
return new Response(JSON.stringify({ error: "Source URL rejected" }), {
|
|
689
|
+
status: 403,
|
|
690
|
+
headers: { "Content-Type": "application/json" }
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
const requestUrl = new URL(request.url);
|
|
695
|
+
const transformOptions = parseTransformParams(requestUrl.searchParams, {
|
|
696
|
+
presets,
|
|
697
|
+
defaults,
|
|
698
|
+
maxWidth,
|
|
699
|
+
maxHeight,
|
|
700
|
+
allowArbitrary
|
|
701
|
+
});
|
|
702
|
+
const cfOptions = presetToCfOptions(transformOptions);
|
|
703
|
+
const hasTransforms = Object.keys(cfOptions).length > 0;
|
|
704
|
+
try {
|
|
705
|
+
const imageRequest = new Request(sourceUrl.toString(), {
|
|
706
|
+
headers: request.headers
|
|
707
|
+
});
|
|
708
|
+
const fetchOptions = {};
|
|
709
|
+
if (hasTransforms) {
|
|
710
|
+
fetchOptions.cf = { image: cfOptions };
|
|
711
|
+
}
|
|
712
|
+
const response = await fetch(imageRequest, fetchOptions);
|
|
713
|
+
if (!response.ok) {
|
|
714
|
+
return new Response(JSON.stringify({ error: "Failed to fetch source image" }), {
|
|
715
|
+
status: response.status,
|
|
716
|
+
headers: { "Content-Type": "application/json" }
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
const headers = new Headers(response.headers);
|
|
720
|
+
headers.set("Cache-Control", cacheControl);
|
|
721
|
+
headers.set("Vary", "Accept");
|
|
722
|
+
return new Response(response.body, {
|
|
723
|
+
status: 200,
|
|
724
|
+
headers
|
|
725
|
+
});
|
|
726
|
+
} catch (error) {
|
|
727
|
+
console.error("Image transform error:", error);
|
|
728
|
+
return new Response(JSON.stringify({ error: "Image transformation failed" }), {
|
|
729
|
+
status: 500,
|
|
730
|
+
headers: { "Content-Type": "application/json" }
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
function variantToPreset(variant) {
|
|
736
|
+
const preset = {};
|
|
737
|
+
if (variant.width !== void 0) preset.width = variant.width;
|
|
738
|
+
if (variant.height !== void 0) preset.height = variant.height;
|
|
739
|
+
if (variant.fit !== void 0) preset.fit = variant.fit;
|
|
740
|
+
if (variant.format !== void 0) preset.format = variant.format;
|
|
741
|
+
if (variant.quality !== void 0) preset.quality = variant.quality;
|
|
742
|
+
if (variant.dpr !== void 0) preset.dpr = variant.dpr;
|
|
743
|
+
if (variant.gravity !== void 0) {
|
|
744
|
+
preset.gravity = variant.gravity;
|
|
745
|
+
}
|
|
746
|
+
if (variant.sharpen !== void 0) preset.sharpen = variant.sharpen;
|
|
747
|
+
if (variant.blur !== void 0) preset.blur = variant.blur;
|
|
748
|
+
if (variant.brightness !== void 0) preset.brightness = variant.brightness;
|
|
749
|
+
if (variant.contrast !== void 0) preset.contrast = variant.contrast;
|
|
750
|
+
if (variant.rotate !== void 0) {
|
|
751
|
+
if ([0, 90, 180, 270].includes(variant.rotate)) {
|
|
752
|
+
preset.rotate = variant.rotate;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
if (variant.metadata !== void 0) preset.metadata = variant.metadata;
|
|
756
|
+
return preset;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// src/binding-types.ts
|
|
760
|
+
var IMAGE_PRESETS = {
|
|
761
|
+
/** Small thumbnail (100x100 cover) */
|
|
762
|
+
thumbnail: {
|
|
763
|
+
width: 100,
|
|
764
|
+
height: 100,
|
|
765
|
+
fit: "cover",
|
|
766
|
+
quality: 80
|
|
767
|
+
},
|
|
768
|
+
/** Medium preview (400x400 contain) */
|
|
769
|
+
preview: {
|
|
770
|
+
width: 400,
|
|
771
|
+
height: 400,
|
|
772
|
+
fit: "contain",
|
|
773
|
+
quality: 85
|
|
774
|
+
},
|
|
775
|
+
/** Large display (1200px wide) */
|
|
776
|
+
large: {
|
|
777
|
+
width: 1200,
|
|
778
|
+
fit: "scale-down",
|
|
779
|
+
quality: 90
|
|
780
|
+
},
|
|
781
|
+
/** Hero banner (1920x1080 cover) */
|
|
782
|
+
hero: {
|
|
783
|
+
width: 1920,
|
|
784
|
+
height: 1080,
|
|
785
|
+
fit: "cover",
|
|
786
|
+
quality: 85
|
|
787
|
+
},
|
|
788
|
+
/** Social share (1200x630 for Open Graph) */
|
|
789
|
+
social: {
|
|
790
|
+
width: 1200,
|
|
791
|
+
height: 630,
|
|
792
|
+
fit: "cover",
|
|
793
|
+
quality: 85
|
|
794
|
+
},
|
|
795
|
+
/** Mobile optimized (640px wide, WebP) */
|
|
796
|
+
mobile: {
|
|
797
|
+
width: 640,
|
|
798
|
+
fit: "scale-down",
|
|
799
|
+
quality: 75,
|
|
800
|
+
format: "image/webp"
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
export {
|
|
804
|
+
CLOUDFLARE_IMAGES_API,
|
|
805
|
+
IMAGE_PRESETS,
|
|
806
|
+
ImageApiError,
|
|
807
|
+
ImageBindingNotFoundError,
|
|
808
|
+
ImageClient,
|
|
809
|
+
ImageConfigError,
|
|
810
|
+
ImageContextError,
|
|
811
|
+
ImageEnvError,
|
|
812
|
+
ImageError,
|
|
813
|
+
ImageNotFoundError,
|
|
814
|
+
ImageTransformError,
|
|
815
|
+
ImageUploadError,
|
|
816
|
+
ImageVariantError,
|
|
817
|
+
createImageClient,
|
|
818
|
+
createImageTransformer,
|
|
819
|
+
defineImage,
|
|
820
|
+
isImageDefinition,
|
|
821
|
+
variantToPreset
|
|
822
|
+
};
|
|
823
|
+
//# sourceMappingURL=index.js.map
|