@hammr/cdn 1.0.0
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/README.md +604 -0
- package/dist/index.cjs +419 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +357 -0
- package/dist/index.d.ts +357 -0
- package/dist/index.js +388 -0
- package/dist/index.js.map +1 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
# @hammr/cdn
|
|
2
|
+
|
|
3
|
+
> Content-addressable CDN with storage abstraction for Cloudflare Workers, Node.js, and beyond.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@hammr/cdn)
|
|
6
|
+
[](https://github.com/hammr/cdn/blob/main/LICENSE)
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **๐ Content-Addressable** - Automatic SHA256 hashing via [@hammr/normalizer](https://www.npmjs.com/package/@hammr/normalizer)
|
|
12
|
+
- **โป๏ธ Idempotent Uploads** - Same content = same hash, stored once
|
|
13
|
+
- **๐ Storage Abstraction** - R2, S3, Memory, FileSystem (bring your own!)
|
|
14
|
+
- **๐ฏ Auto Content-Type Detection** - 40+ file types recognized automatically
|
|
15
|
+
- **โก Immutable Artifacts** - Hash-based URLs with aggressive caching
|
|
16
|
+
- **๐ HTTP Request Handler** - Drop-in handler for Cloudflare Workers
|
|
17
|
+
- **๐ Production-Ready** - CORS, ETag, Cache-Control, error handling
|
|
18
|
+
- **๐ฆ Zero Config** - Works out of the box with sensible defaults
|
|
19
|
+
- **๐งช 100% Test Coverage** - Fully tested and type-safe
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install @sygnl/cdn
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
### Cloudflare Workers (R2 Storage)
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import { CDN, R2Storage } from '@sygnl/cdn';
|
|
33
|
+
|
|
34
|
+
export default {
|
|
35
|
+
async fetch(request: Request, env: Env) {
|
|
36
|
+
const cdn = new CDN({
|
|
37
|
+
storage: new R2Storage(env.ARTIFACTS),
|
|
38
|
+
baseUrl: 'https://cdn.example.com'
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return cdn.handleRequest(request);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Programmatic Usage
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { CDN, MemoryStorage } from '@sygnl/cdn';
|
|
50
|
+
|
|
51
|
+
const cdn = new CDN({
|
|
52
|
+
storage: new MemoryStorage(),
|
|
53
|
+
baseUrl: 'https://cdn.example.com'
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Upload artifact
|
|
57
|
+
const imageBytes = await fetch('https://example.com/logo.png').then(r => r.arrayBuffer());
|
|
58
|
+
const result = await cdn.put(imageBytes, {
|
|
59
|
+
filename: 'logo.png'
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
console.log(result.url);
|
|
63
|
+
// โ https://cdn.example.com/a/5e884898da28047151d0e56f8dc629...png
|
|
64
|
+
|
|
65
|
+
// Retrieve artifact
|
|
66
|
+
const artifact = await cdn.get(result.hash);
|
|
67
|
+
console.log(artifact.metadata.contentType); // โ image/png
|
|
68
|
+
|
|
69
|
+
// Delete artifact
|
|
70
|
+
await cdn.delete(result.hash);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## How It Works
|
|
74
|
+
|
|
75
|
+
1. **Upload** โ Content is hashed with SHA256 (content-addressable)
|
|
76
|
+
2. **Store** โ Artifact stored with hash as key (idempotent)
|
|
77
|
+
3. **Serve** โ Immutable URL with aggressive caching
|
|
78
|
+
4. **Deduplicate** โ Same content = same hash = stored once
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
// Upload the same file twice
|
|
82
|
+
const upload1 = await cdn.put(bytes, { filename: 'logo.png' });
|
|
83
|
+
const upload2 = await cdn.put(bytes, { filename: 'logo.png' });
|
|
84
|
+
|
|
85
|
+
console.log(upload1.hash === upload2.hash); // โ true
|
|
86
|
+
console.log(upload1.created); // โ true (new upload)
|
|
87
|
+
console.log(upload2.created); // โ false (already existed)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## API Reference
|
|
91
|
+
|
|
92
|
+
### `CDN`
|
|
93
|
+
|
|
94
|
+
#### Constructor
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
new CDN(options: CDNOptions)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Options:**
|
|
101
|
+
- `storage: StorageAdapter` - Storage backend (R2, Memory, etc.)
|
|
102
|
+
- `baseUrl: string` - Base URL for generating artifact URLs
|
|
103
|
+
- `cacheMaxAge?: number` - Cache-Control max-age in seconds (default: 31536000 = 1 year)
|
|
104
|
+
- `defaultContentType?: string` - Fallback content type (default: `application/octet-stream`)
|
|
105
|
+
- `cors?: boolean` - Enable CORS headers (default: `true`)
|
|
106
|
+
|
|
107
|
+
#### Methods
|
|
108
|
+
|
|
109
|
+
##### `put(content, metadata?): Promise<UploadResult>`
|
|
110
|
+
|
|
111
|
+
Upload an artifact and get content-addressable URL.
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
const result = await cdn.put(imageBytes, {
|
|
115
|
+
filename: 'logo.png', // Optional: filename (used for content-type detection)
|
|
116
|
+
contentType: 'image/png', // Optional: override content-type
|
|
117
|
+
customMetadata: { // Optional: custom key-value metadata
|
|
118
|
+
author: 'John Doe',
|
|
119
|
+
version: '1.0'
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
console.log(result);
|
|
124
|
+
// {
|
|
125
|
+
// hash: '5e884898da28047151d0e56f8dc629...',
|
|
126
|
+
// url: 'https://cdn.example.com/a/5e88489...png',
|
|
127
|
+
// created: true,
|
|
128
|
+
// metadata: {
|
|
129
|
+
// contentType: 'image/png',
|
|
130
|
+
// filename: 'logo.png',
|
|
131
|
+
// size: 12345,
|
|
132
|
+
// uploadedAt: 1609459200000,
|
|
133
|
+
// customMetadata: { author: 'John Doe', version: '1.0' }
|
|
134
|
+
// }
|
|
135
|
+
// }
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
##### `get(hash): Promise<StoredArtifact | null>`
|
|
139
|
+
|
|
140
|
+
Retrieve an artifact by hash.
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
const artifact = await cdn.get('5e884898da28047151d0e56f8dc629...');
|
|
144
|
+
|
|
145
|
+
if (artifact) {
|
|
146
|
+
console.log(artifact.hash); // SHA256 hash
|
|
147
|
+
console.log(artifact.body); // ArrayBuffer | ReadableStream
|
|
148
|
+
console.log(artifact.metadata); // Metadata object
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
##### `delete(hash): Promise<boolean>`
|
|
153
|
+
|
|
154
|
+
Delete an artifact by hash.
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
const deleted = await cdn.delete('5e884898da28047151d0e56f8dc629...');
|
|
158
|
+
console.log(deleted); // true if deleted, false if not found
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
##### `exists(hash): Promise<boolean>`
|
|
162
|
+
|
|
163
|
+
Check if an artifact exists.
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
const exists = await cdn.exists('5e884898da28047151d0e56f8dc629...');
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
##### `list(options?): Promise<string[]>`
|
|
170
|
+
|
|
171
|
+
List all artifact hashes (if supported by storage adapter).
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
const hashes = await cdn.list({ limit: 100 });
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
##### `handleRequest(request): Promise<Response>`
|
|
178
|
+
|
|
179
|
+
Handle HTTP requests (for Cloudflare Workers, Express, etc.).
|
|
180
|
+
|
|
181
|
+
**Supported Routes:**
|
|
182
|
+
- `PUT /artifact?filename=logo.png` - Upload artifact
|
|
183
|
+
- `GET /a/:hash` or `GET /a/:hash.ext` - Retrieve artifact
|
|
184
|
+
- `DELETE /a/:hash` - Delete artifact
|
|
185
|
+
- `OPTIONS *` - CORS preflight
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
// Cloudflare Workers
|
|
189
|
+
export default {
|
|
190
|
+
async fetch(request: Request, env: Env) {
|
|
191
|
+
const cdn = new CDN({
|
|
192
|
+
storage: new R2Storage(env.ARTIFACTS),
|
|
193
|
+
baseUrl: 'https://cdn.example.com'
|
|
194
|
+
});
|
|
195
|
+
return cdn.handleRequest(request);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Storage Adapters
|
|
201
|
+
|
|
202
|
+
### R2Storage (Cloudflare R2)
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
import { CDN, R2Storage } from '@sygnl/cdn';
|
|
206
|
+
|
|
207
|
+
const cdn = new CDN({
|
|
208
|
+
storage: new R2Storage(env.ARTIFACTS), // R2 binding from Cloudflare Workers
|
|
209
|
+
baseUrl: 'https://cdn.example.com'
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**Requirements:**
|
|
214
|
+
- Cloudflare Workers environment
|
|
215
|
+
- R2 bucket binding in `wrangler.toml`:
|
|
216
|
+
|
|
217
|
+
```toml
|
|
218
|
+
[[r2_buckets]]
|
|
219
|
+
binding = "ARTIFACTS"
|
|
220
|
+
bucket_name = "my-cdn-artifacts"
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### MemoryStorage (Development/Testing)
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
import { CDN, MemoryStorage } from '@sygnl/cdn';
|
|
227
|
+
|
|
228
|
+
const cdn = new CDN({
|
|
229
|
+
storage: new MemoryStorage(),
|
|
230
|
+
baseUrl: 'https://cdn.example.com'
|
|
231
|
+
});
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**Note:** Data is lost on restart. Use for testing only.
|
|
235
|
+
|
|
236
|
+
### Custom Storage Adapter
|
|
237
|
+
|
|
238
|
+
Implement the `StorageAdapter` interface:
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
import type { StorageAdapter, StoredArtifact, ArtifactMetadata } from '@hammr/cdn';
|
|
242
|
+
|
|
243
|
+
class S3Storage implements StorageAdapter {
|
|
244
|
+
async put(hash: string, content: ArrayBuffer | Uint8Array, metadata?: ArtifactMetadata): Promise<void> {
|
|
245
|
+
// Upload to S3
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async get(hash: string): Promise<StoredArtifact | null> {
|
|
249
|
+
// Retrieve from S3
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async delete(hash: string): Promise<boolean> {
|
|
253
|
+
// Delete from S3
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async exists(hash: string): Promise<boolean> {
|
|
257
|
+
// Check if exists in S3
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async list?(options?: { limit?: number; cursor?: string }): Promise<string[]> {
|
|
261
|
+
// List all hashes (optional)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const cdn = new CDN({
|
|
266
|
+
storage: new S3Storage(),
|
|
267
|
+
baseUrl: 'https://cdn.example.com'
|
|
268
|
+
});
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Content-Type Detection
|
|
272
|
+
|
|
273
|
+
Automatic detection for 40+ file types:
|
|
274
|
+
|
|
275
|
+
| Extension | Content-Type |
|
|
276
|
+
|-----------|--------------|
|
|
277
|
+
| `.png`, `.jpg`, `.gif`, `.webp` | `image/*` |
|
|
278
|
+
| `.pdf`, `.json`, `.xml`, `.txt` | `application/*` or `text/*` |
|
|
279
|
+
| `.js`, `.mjs`, `.ts`, `.wasm` | `application/javascript`, etc. |
|
|
280
|
+
| `.mp3`, `.mp4`, `.webm`, `.ogg` | `audio/*` or `video/*` |
|
|
281
|
+
| `.woff`, `.woff2`, `.ttf`, `.otf` | `font/*` |
|
|
282
|
+
|
|
283
|
+
**Override detection:**
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
await cdn.put(bytes, {
|
|
287
|
+
filename: 'data.txt',
|
|
288
|
+
contentType: 'application/json' // Override detected type
|
|
289
|
+
});
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## HTTP API
|
|
293
|
+
|
|
294
|
+
### Upload Artifact
|
|
295
|
+
|
|
296
|
+
**Request:**
|
|
297
|
+
```http
|
|
298
|
+
PUT /artifact?filename=logo.png HTTP/1.1
|
|
299
|
+
Content-Type: image/png
|
|
300
|
+
|
|
301
|
+
[binary data]
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
**Response:**
|
|
305
|
+
```json
|
|
306
|
+
{
|
|
307
|
+
"hash": "5e884898da28047151d0e56f8dc6296...",
|
|
308
|
+
"url": "https://cdn.example.com/a/5e884898...png",
|
|
309
|
+
"created": true,
|
|
310
|
+
"metadata": {
|
|
311
|
+
"contentType": "image/png",
|
|
312
|
+
"filename": "logo.png",
|
|
313
|
+
"size": 12345,
|
|
314
|
+
"uploadedAt": 1609459200000
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Retrieve Artifact
|
|
320
|
+
|
|
321
|
+
**Request:**
|
|
322
|
+
```http
|
|
323
|
+
GET /a/5e884898da28047151d0e56f8dc6296...png HTTP/1.1
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**Response:**
|
|
327
|
+
```http
|
|
328
|
+
HTTP/1.1 200 OK
|
|
329
|
+
Content-Type: image/png
|
|
330
|
+
Cache-Control: public, max-age=31536000, immutable
|
|
331
|
+
ETag: "5e884898da28047151d0e56f8dc6296..."
|
|
332
|
+
Access-Control-Allow-Origin: *
|
|
333
|
+
|
|
334
|
+
[binary data]
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Delete Artifact
|
|
338
|
+
|
|
339
|
+
**Request:**
|
|
340
|
+
```http
|
|
341
|
+
DELETE /a/5e884898da28047151d0e56f8dc6296... HTTP/1.1
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
**Response:**
|
|
345
|
+
```json
|
|
346
|
+
{
|
|
347
|
+
"deleted": true,
|
|
348
|
+
"hash": "5e884898da28047151d0e56f8dc6296..."
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## Examples
|
|
353
|
+
|
|
354
|
+
### Upload from Form Data
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
// Client-side
|
|
358
|
+
const formData = new FormData();
|
|
359
|
+
formData.append('file', fileInput.files[0]);
|
|
360
|
+
|
|
361
|
+
const response = await fetch('https://cdn.example.com/artifact?filename=logo.png', {
|
|
362
|
+
method: 'PUT',
|
|
363
|
+
body: await fileInput.files[0].arrayBuffer()
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
const result = await response.json();
|
|
367
|
+
console.log(result.url); // Use this URL in <img> tags, etc.
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### Bulk Upload
|
|
371
|
+
|
|
372
|
+
```typescript
|
|
373
|
+
const files = ['logo.png', 'icon.svg', 'banner.jpg'];
|
|
374
|
+
|
|
375
|
+
const results = await Promise.all(
|
|
376
|
+
files.map(async (filename) => {
|
|
377
|
+
const bytes = await fs.readFile(filename);
|
|
378
|
+
return cdn.put(bytes, { filename });
|
|
379
|
+
})
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
console.log(results.map(r => r.url));
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Custom Metadata
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
const result = await cdn.put(imageBytes, {
|
|
389
|
+
filename: 'product.jpg',
|
|
390
|
+
customMetadata: {
|
|
391
|
+
productId: 'prod_123',
|
|
392
|
+
uploadedBy: 'user_456',
|
|
393
|
+
version: '2.0'
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// Retrieve metadata later
|
|
398
|
+
const artifact = await cdn.get(result.hash);
|
|
399
|
+
console.log(artifact.metadata.customMetadata.productId); // โ prod_123
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### Verify Upload Integrity
|
|
403
|
+
|
|
404
|
+
```typescript
|
|
405
|
+
import { sha256 } from '@sygnl/normalizer';
|
|
406
|
+
|
|
407
|
+
// Client computes hash before upload
|
|
408
|
+
const clientHash = await sha256(Array.from(new Uint8Array(fileBytes))
|
|
409
|
+
.map(b => String.fromCharCode(b)).join(''));
|
|
410
|
+
|
|
411
|
+
// Upload
|
|
412
|
+
const result = await cdn.put(fileBytes, { filename: 'file.dat' });
|
|
413
|
+
|
|
414
|
+
// Verify server returned same hash
|
|
415
|
+
if (result.hash === clientHash) {
|
|
416
|
+
console.log('โ
Upload verified - content matches hash');
|
|
417
|
+
} else {
|
|
418
|
+
console.error('โ Upload corrupted - hashes do not match');
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
## Configuration
|
|
423
|
+
|
|
424
|
+
### Cache Strategy
|
|
425
|
+
|
|
426
|
+
```typescript
|
|
427
|
+
const cdn = new CDN({
|
|
428
|
+
storage: new R2Storage(env.ARTIFACTS),
|
|
429
|
+
baseUrl: 'https://cdn.example.com',
|
|
430
|
+
cacheMaxAge: 31536000, // 1 year (default)
|
|
431
|
+
});
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
**Cache-Control header:**
|
|
435
|
+
```
|
|
436
|
+
Cache-Control: public, max-age=31536000, immutable
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
**Why immutable?**
|
|
440
|
+
Content-addressable URLs never change. The hash IS the content. Safe to cache forever.
|
|
441
|
+
|
|
442
|
+
### Disable CORS
|
|
443
|
+
|
|
444
|
+
```typescript
|
|
445
|
+
const cdn = new CDN({
|
|
446
|
+
storage: new R2Storage(env.ARTIFACTS),
|
|
447
|
+
baseUrl: 'https://cdn.example.com',
|
|
448
|
+
cors: false, // Disable CORS headers
|
|
449
|
+
});
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### Custom Base URL with Path
|
|
453
|
+
|
|
454
|
+
```typescript
|
|
455
|
+
const cdn = new CDN({
|
|
456
|
+
storage: new R2Storage(env.ARTIFACTS),
|
|
457
|
+
baseUrl: 'https://example.com/cdn', // Trailing slash removed automatically
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
const result = await cdn.put(bytes, { filename: 'logo.png' });
|
|
461
|
+
console.log(result.url);
|
|
462
|
+
// โ https://example.com/cdn/a/5e884898...png
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
## Production Deployment
|
|
466
|
+
|
|
467
|
+
### Cloudflare Workers
|
|
468
|
+
|
|
469
|
+
**1. Install dependencies:**
|
|
470
|
+
|
|
471
|
+
```bash
|
|
472
|
+
npm install @hammr/cdn
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
**2. Configure `wrangler.toml`:**
|
|
476
|
+
|
|
477
|
+
```toml
|
|
478
|
+
name = "cdn-worker"
|
|
479
|
+
main = "src/index.ts"
|
|
480
|
+
compatibility_date = "2024-01-01"
|
|
481
|
+
|
|
482
|
+
[[r2_buckets]]
|
|
483
|
+
binding = "ARTIFACTS"
|
|
484
|
+
bucket_name = "my-cdn-artifacts"
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
**3. Create worker:**
|
|
488
|
+
|
|
489
|
+
```typescript
|
|
490
|
+
// src/index.ts
|
|
491
|
+
import { CDN, R2Storage } from '@hammr/cdn';
|
|
492
|
+
|
|
493
|
+
interface Env {
|
|
494
|
+
ARTIFACTS: R2Bucket;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export default {
|
|
498
|
+
async fetch(request: Request, env: Env): Promise<Response> {
|
|
499
|
+
const cdn = new CDN({
|
|
500
|
+
storage: new R2Storage(env.ARTIFACTS),
|
|
501
|
+
baseUrl: 'https://cdn.example.com'
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
return cdn.handleRequest(request);
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
**4. Deploy:**
|
|
510
|
+
|
|
511
|
+
```bash
|
|
512
|
+
npx wrangler deploy
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
### Custom Domain
|
|
516
|
+
|
|
517
|
+
In Cloudflare dashboard:
|
|
518
|
+
1. Workers & Pages โ your worker โ Settings โ Domains & Routes
|
|
519
|
+
2. Add custom domain: `cdn.example.com`
|
|
520
|
+
3. Update `baseUrl` in code to match
|
|
521
|
+
|
|
522
|
+
## Performance
|
|
523
|
+
|
|
524
|
+
### Benchmarks (Cloudflare Workers + R2)
|
|
525
|
+
|
|
526
|
+
- **Upload (PUT):** ~50ms (includes SHA256 hashing + R2 write)
|
|
527
|
+
- **Retrieve (GET):** ~10-30ms (R2 read, first request)
|
|
528
|
+
- **Retrieve (cached):** ~1-5ms (edge cache hit)
|
|
529
|
+
- **Delete:** ~20ms (R2 delete)
|
|
530
|
+
|
|
531
|
+
### Optimization Tips
|
|
532
|
+
|
|
533
|
+
1. **Use R2 for production** - Fast, cheap, globally distributed
|
|
534
|
+
2. **Enable Cloudflare Cache** - Artifacts cached at edge automatically
|
|
535
|
+
3. **Use HTTP/2** - Multiplexing for bulk uploads
|
|
536
|
+
4. **Compress before upload** - Use gzip/brotli for compressible files
|
|
537
|
+
|
|
538
|
+
## Troubleshooting
|
|
539
|
+
|
|
540
|
+
### "Cannot find module '@hammr/normalizer'"
|
|
541
|
+
|
|
542
|
+
The normalizer package is a required peer dependency:
|
|
543
|
+
|
|
544
|
+
```bash
|
|
545
|
+
npm install @hammr/normalizer
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
### R2 binding not found
|
|
549
|
+
|
|
550
|
+
Ensure `wrangler.toml` has R2 binding:
|
|
551
|
+
|
|
552
|
+
```toml
|
|
553
|
+
[[r2_buckets]]
|
|
554
|
+
binding = "ARTIFACTS"
|
|
555
|
+
bucket_name = "my-bucket"
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
And your worker receives it in `env`:
|
|
559
|
+
|
|
560
|
+
```typescript
|
|
561
|
+
interface Env {
|
|
562
|
+
ARTIFACTS: R2Bucket;
|
|
563
|
+
}
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
### Content-Type not detected
|
|
567
|
+
|
|
568
|
+
Specify explicitly:
|
|
569
|
+
|
|
570
|
+
```typescript
|
|
571
|
+
await cdn.put(bytes, {
|
|
572
|
+
contentType: 'application/custom'
|
|
573
|
+
});
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
### CORS errors
|
|
577
|
+
|
|
578
|
+
Ensure `cors: true` (default):
|
|
579
|
+
|
|
580
|
+
```typescript
|
|
581
|
+
const cdn = new CDN({
|
|
582
|
+
storage: new R2Storage(env.ARTIFACTS),
|
|
583
|
+
baseUrl: 'https://cdn.example.com',
|
|
584
|
+
cors: true, // Enable CORS (default)
|
|
585
|
+
});
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
## Related Packages
|
|
589
|
+
|
|
590
|
+
- [@sygnl/normalizer](https://www.npmjs.com/package/@sygnl/normalizer) - PII normalization & SHA256 hashing (used internally)
|
|
591
|
+
- [@sygnl/identity-manager](https://www.npmjs.com/package/@sygnl/identity-manager) - Session & identity tracking
|
|
592
|
+
- [@sygnl/health-check](https://www.npmjs.com/package/@sygnl/health-check) - Production observability
|
|
593
|
+
|
|
594
|
+
## License
|
|
595
|
+
|
|
596
|
+
UNLICENSED
|
|
597
|
+
|
|
598
|
+
## Contributing
|
|
599
|
+
|
|
600
|
+
Issues and PRs welcome! This package is part of the [Sygnl](https://sygnl.io) ecosystem.
|
|
601
|
+
|
|
602
|
+
---
|
|
603
|
+
|
|
604
|
+
**Built with โค๏ธ by Edge Foundry, Inc.**
|