{"_id":"@azurajs/cdn","name":"@azurajs/cdn","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"@azurajs/cdn","version":"1.0.0","description":"A full-featured CDN module for AzuraJS with caching, compression, signed URLs, and more.","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","type":"module","scripts":{"build":"tsup","prepublishOnly":"bun run build"},"keywords":["typescript","framework","web","api","rest","cdn","server","backend","bun","node","zero-dependency","lightweight","fast"],"author":{"name":"0xviny.dev@gmail.com"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/azurajs/cdn.git"},"bugs":{"url":"https://github.com/azurajs/cdn/issues"},"homepage":"https://github.com/azurajs/cdn#readme","engines":{"node":">=18.0.0","bun":">=1.0.0"},"devDependencies":{"@types/bun":"latest","tsup":"^8.5.1"},"peerDependencies":{"typescript":"^5"},"dependencies":{"azurajs":"^2.6.0"},"_id":"@azurajs/cdn@1.0.0","gitHead":"947af7e9ca4f97b97735e42a6fdafa973932c826","_nodeVersion":"22.20.0","_npmVersion":"10.9.3","dist":{"integrity":"sha512-mTVlIGHc6T4yLMqfWOv/O4Hi8OVtNnIIRE8IoVkfSp+kPVU3+AMNnngy68ZKyJicCInfrObv6FM7OyaBdDsySw==","shasum":"e100c9e4ec10ec1cbb079888de49374908013154","tarball":"https://registry.npmjs.org/@azurajs/cdn/-/cdn-1.0.0.tgz","fileCount":25,"unpackedSize":395195,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCnnelzfJbh3oKIm0JYjxLsxxxCgggEIMa5BKX9PXjjzgIhAJf5nD3GqSaSrysy5KKHK/ZrVZFne/TXgJaaj2cDBv1y"}]},"_npmUser":{"name":"0xviny","email":"0xviny.dev@gmail.com"},"directories":{},"maintainers":[{"name":"0xviny","email":"0xviny.dev@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/cdn_1.0.0_1769542694253_0.468071687535242"},"_hasShrinkwrap":false}},"time":{"created":"2026-01-27T19:38:14.174Z","1.0.0":"2026-01-27T19:38:14.434Z","modified":"2026-01-27T19:38:14.630Z"},"maintainers":[{"name":"0xviny","email":"0xviny.dev@gmail.com"}],"description":"A full-featured CDN module for AzuraJS with caching, compression, signed URLs, and more.","homepage":"https://github.com/azurajs/cdn#readme","keywords":["typescript","framework","web","api","rest","cdn","server","backend","bun","node","zero-dependency","lightweight","fast"],"repository":{"type":"git","url":"git+https://github.com/azurajs/cdn.git"},"author":{"name":"0xviny.dev@gmail.com"},"bugs":{"url":"https://github.com/azurajs/cdn/issues"},"license":"MIT","readme":"# @azurajs/cdn\n\nA full-featured CDN module for AzuraJS with caching, compression, signed URLs, and more.\n\n## 📋 Table of Contents\n\n- [Basic Concepts](#-basic-concepts)\n- [Installation](#-installation)\n- [Quick Start](#-quick-start)\n- [How It Works](#-how-it-works)\n- [Configuration](#-configuration)\n- [API](#-api)\n- [Internal Endpoints](#-internal-endpoints)\n- [HTTP Headers](#-http-headers)\n- [Examples](#-examples)\n\n---\n\n## 📖 Basic Concepts\n\n### What is a CDN?\n\nA CDN (Content Delivery Network) is a server that sits between clients and your origin server, caching responses to serve them faster on subsequent requests.\n\n```\nClient → CDN (cache) → Origin Server\n           ↓\n       Cached response (if available)\n```\n\n### How does AzuraJS CDN work?\n\n1. **Client makes request** to CDN (e.g., `http://localhost:3001/api/users`)\n2. **CDN checks cache** (memory → disk)\n3. **If found (HIT)**: Returns immediately with `x-cache: HIT-MEMORY` or `HIT-DISK` header\n4. **If not found (MISS)**: Fetches from origin server, stores in cache, returns with `x-cache: MISS`\n\n### Uploading files to CDN?\n\nThe CDN **does not store files directly** - it's a **caching proxy**. You don't \"upload\" files to it. The CDN automatically caches responses from your origin server when clients make requests.\n\n**Flow for serving static files:**\n\n1. Configure your origin server to serve files (e.g., route `/files/:filename`)\n2. Configure CDN to point to your origin server\n3. Clients access files via CDN: `http://cdn.yoursite.com/files/document.pdf`\n4. CDN automatically caches after first request\n\n---\n\n## 🚀 Installation\n\n```bash\nnpm install @azurajs/cdn\n```\n\n---\n\n## ⚡ Quick Start\n\n### Standalone CDN Server\n\n```typescript\nimport { createCDN } from \"@azurajs/cdn\";\n\nconst cdn = createCDN({\n  // Origin server URL (your API)\n  origin: \"http://localhost:3000\",\n\n  cache: {\n    maxMemoryBytes: 256 * 1024 * 1024, // 256MB RAM\n    ttl: 300, // 5 minutes default\n    disk: {\n      enabled: true,\n      path: \"./cache\",\n      maxSizeBytes: 1024 * 1024 * 1024, // 1GB disk\n    },\n  },\n\n  compression: {\n    enabled: true,\n    gzip: true,\n    brotli: true,\n  },\n\n  signedUrls: {\n    secret: \"your-secret-key\",\n    pathPrefix: \"/private/\",\n  },\n\n  rateLimit: {\n    enabled: true,\n    windowMs: 60000, // 1 minute\n    max: 1000, // 1000 req/min\n  },\n});\n\n// Events\ncdn.on(\"hit\", (e) => console.log(`HIT: ${e.key} (${e.source})`));\ncdn.on(\"miss\", (e) => console.log(`MISS: ${e.key}`));\n\n// Start\ncdn.listen(3001);\n```\n\n### AzuraJS Plugin\n\n```typescript\nimport { AzuraClient } from \"azurajs\";\nimport { cdnPlugin } from \"@azurajs/cdn\";\n\nconst app = new AzuraClient();\n\nconst cdn = app.use(\n  cdnPlugin({\n    origin: \"http://localhost:3000\",\n    cache: { ttl: 300 },\n  }),\n);\n\ncdn.listen(3001);\n```\n\n---\n\n## 🔧 How It Works\n\n### Request Flow Diagram\n\n```\n┌─────────────────────────────────────────────────────────────────────┐\n│                          CLIENT REQUEST                              │\n└─────────────────────────────────────────────────────────────────────┘\n                                    │\n                                    ▼\n┌─────────────────────────────────────────────────────────────────────┐\n│                           RATE LIMITING                              │\n│              Checks if client exceeded request limit                 │\n│                    429 Too Many Requests if yes                      │\n└─────────────────────────────────────────────────────────────────────┘\n                                    │\n                                    ▼\n┌─────────────────────────────────────────────────────────────────────┐\n│                      SIGNED URL VERIFICATION                         │\n│          If path starts with /private/, validates signature          │\n│                       403 Forbidden if invalid                       │\n└─────────────────────────────────────────────────────────────────────┘\n                                    │\n                                    ▼\n┌─────────────────────────────────────────────────────────────────────┐\n│                          L1 CACHE (MEMORY)                           │\n│                 Checks if exists in RAM cache                        │\n│               Header: x-cache: HIT-MEMORY if found                   │\n└─────────────────────────────────────────────────────────────────────┘\n                    │                              │\n                 (HIT)                          (MISS)\n                    │                              ▼\n                    │         ┌───────────────────────────────────────┐\n                    │         │           L2 CACHE (DISK)             │\n                    │         │    Checks if exists in local cache    │\n                    │         │     Header: x-cache: HIT-DISK         │\n                    │         └───────────────────────────────────────┘\n                    │                     │              │\n                    │                  (HIT)          (MISS)\n                    │                     │              ▼\n                    │                     │  ┌────────────────────────┐\n                    │                     │  │     ORIGIN SERVER      │\n                    │                     │  │   Fetches real data    │\n                    │                     │  │   Stores in caches     │\n                    │                     │  │  Header: x-cache: MISS │\n                    │                     │  └────────────────────────┘\n                    ▼                     ▼              │\n┌─────────────────────────────────────────────────────────────────────┐\n│                       CONDITIONAL REQUEST                            │\n│        Checks If-None-Match/If-Modified-Since from client            │\n│                   304 Not Modified if unchanged                      │\n└─────────────────────────────────────────────────────────────────────┘\n                                    │\n                                    ▼\n┌─────────────────────────────────────────────────────────────────────┐\n│                          COMPRESSION                                 │\n│             Checks Accept-Encoding from client                       │\n│           Serves brotli > gzip > raw based on support                │\n│             Header: Content-Encoding: br or gzip                     │\n└─────────────────────────────────────────────────────────────────────┘\n                                    │\n                                    ▼\n┌─────────────────────────────────────────────────────────────────────┐\n│                       CLIENT RESPONSE                                │\n└─────────────────────────────────────────────────────────────────────┘\n```\n\n### Cache Hierarchy\n\n| Level | Storage          | Speed | Capacity            |\n| ----- | ---------------- | ----- | ------------------- |\n| L1    | RAM Memory       | ~1ms  | 256MB (default)     |\n| L2    | Disk SSD/HDD     | ~10ms | 1GB+ (configurable) |\n| L3    | Redis (optional) | ~5ms  | Unlimited           |\n\n---\n\n## ⚙️ Configuration\n\n### CDNConfig\n\n```typescript\ninterface CDNConfig {\n  // Origin server URL (REQUIRED)\n  origin: string;\n\n  cache?: {\n    // Maximum memory cache size (bytes)\n    // Default: 512MB (536870912)\n    maxMemoryBytes?: number;\n\n    // Default TTL in seconds (if origin doesn't send Cache-Control)\n    // Default: 300 (5 minutes)\n    ttl?: number;\n\n    // Serve stale content while revalidating in background\n    staleWhileRevalidate?: boolean;\n\n    // Seconds to serve stale content if origin fails\n    // Default: undefined (disabled)\n    staleIfError?: number;\n\n    // Disk cache configuration\n    disk?: {\n      enabled?: boolean;\n      path?: string;\n      maxSizeBytes?: number; // Default: 1GB\n    };\n\n    // Redis configuration (optional)\n    redis?: {\n      enabled?: boolean;\n      host?: string;\n      port?: number;\n      password?: string;\n      db?: number;\n      keyPrefix?: string;\n    };\n  };\n\n  compression?: {\n    // Enable compression (default: true)\n    enabled?: boolean;\n\n    // Enable gzip (default: true)\n    gzip?: boolean;\n\n    // Enable brotli (default: true)\n    brotli?: boolean;\n\n    // Minimum size to compress (bytes)\n    // Default: 1024 (1KB)\n    minSize?: number;\n\n    // MIME types to compress\n    // Default: text/*, application/json, application/javascript, etc\n    mimeTypes?: string[];\n  };\n\n  signedUrls?: {\n    // Secret key for signing URLs (REQUIRED if enabled)\n    secret: string;\n\n    // Default TTL in seconds (default: 60)\n    defaultTtl?: number;\n\n    // Path prefix requiring signature (default: \"/private/\")\n    pathPrefix?: string;\n  };\n\n  rateLimit?: {\n    // Enable rate limiting\n    enabled?: boolean;\n\n    // Time window in ms (default: 60000 = 1 min)\n    windowMs?: number;\n\n    // Maximum requests per window (default: 1000)\n    max?: number;\n\n    // Custom function to generate key (default: client IP)\n    keyGenerator?: (req: IncomingMessage) => string;\n  };\n\n  server?: {\n    // CDN port (default: 3001)\n    port?: number;\n\n    // Host (default: \"0.0.0.0\")\n    host?: string;\n  };\n}\n```\n\n---\n\n## 📚 API\n\n### Cache Operations\n\n```typescript\n// Exact purge\ncdn.purge(\"/api/users/123\");\n\n// Pattern purge (wildcard)\ncdn.purge(\"/api/users/*\", { pattern: true });\n\n// Purge by tags\ncdn.purgeTags([\"users\", \"auth\"]);\n\n// Clear all cache\ncdn.purgeAll();\n\n// Remove expired entries\nawait cdn.cleanup();\n\n// Pre-populate cache (warming)\nawait cdn.warm([\"/api/popular\", \"/api/featured\"], { concurrency: 5 });\n```\n\n### Signed URLs\n\n```typescript\n// Generate signed URL (expires in 5 minutes)\nconst signedUrl = cdn.generateSignedUrl(\"/private/document.pdf\", 300);\n// Result: /private/document.pdf?expires=1706047200&sig=abc123...\n\n// Access via CDN\n// GET http://localhost:3001/private/document.pdf?expires=1706047200&sig=abc123...\n```\n\n### Events\n\n```typescript\n// Cache hit\ncdn.on(\"hit\", (event) => {\n  console.log(`HIT: ${event.key}`);\n  console.log(`Source: ${event.source}`); // \"memory\" | \"disk\" | \"redis\"\n});\n\n// Cache miss\ncdn.on(\"miss\", (event) => {\n  console.log(`MISS: ${event.key}`);\n});\n\n// Purge\ncdn.on(\"purge\", (event) => {\n  console.log(`PURGED: ${event.key}`);\n  console.log(`Count: ${event.data?.count}`);\n});\n\n// Error\ncdn.on(\"error\", (event) => {\n  console.error(\"CDN Error:\", event.data);\n});\n\n// Compression applied\ncdn.on(\"compress\", (event) => {\n  console.log(`Compressed: ${event.key}`);\n});\n\n// Cache warming\ncdn.on(\"warm\", (event) => {\n  console.log(`Warmed: ${event.key}`);\n});\n```\n\n### Metrics\n\n```typescript\nconst metrics = cdn.getMetrics();\n// {\n//   requests: 12345,\n//   hitRate: \"94.50%\",\n//   hits: { memory: 10000, disk: 1650 },\n//   misses: 695,\n//   errors: 0,\n//   bandwidth: {\n//     served: \"1.2 GB\",    // Data sent to client\n//     saved: \"15.3 GB\"     // Data saved (not fetched from origin)\n//   },\n//   avgLatency: \"12.50ms\",\n//   uptime: \"3600s\"\n// }\n\n// Cache statistics\nconst memStats = cdn.getMemoryStats();\n// { entries: 100, currentBytes: 52428800, maxBytes: 268435456, usagePercent: \"19.53\" }\n\nconst diskStats = await cdn.getDiskStats();\n// { entries: 50, currentBytes: 104857600, maxBytes: 1073741824, usagePercent: \"9.77\" }\n\n// Reset metrics\ncdn.resetMetrics();\n```\n\n---\n\n## 🔌 Internal Endpoints\n\nWhen the CDN server is running (port 3001), these endpoints are available:\n\n| Endpoint     | Method | Description                       |\n| ------------ | ------ | --------------------------------- |\n| `/__stats`   | GET    | Full statistics (cache + metrics) |\n| `/__metrics` | GET    | Performance metrics only          |\n| `/__health`  | GET    | Health check                      |\n| `/__purge`   | POST   | Purge cache                       |\n| `/__clear`   | POST   | Clear all cache                   |\n\n### Usage Examples\n\n```bash\n# Health check\ncurl http://localhost:3001/__health\n\n# Statistics\ncurl http://localhost:3001/__stats\n\n# Exact purge\ncurl -X POST http://localhost:3001/__purge \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"path\": \"/api/users/123\"}'\n\n# Wildcard purge\ncurl -X POST http://localhost:3001/__purge \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"path\": \"/api/users/*\", \"pattern\": true}'\n\n# Purge by tags\ncurl -X POST http://localhost:3001/__purge \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"tags\": [\"users\", \"posts\"]}'\n\n# Clear all\ncurl -X POST http://localhost:3001/__clear\n```\n\n---\n\n## 📡 HTTP Headers\n\n### Response Headers\n\n| Header             | Value                           | Description                        |\n| ------------------ | ------------------------------- | ---------------------------------- |\n| `x-cache`          | `HIT-MEMORY`                    | Served from memory cache           |\n| `x-cache`          | `HIT-DISK`                      | Served from disk cache             |\n| `x-cache`          | `MISS`                          | Fetched from origin                |\n| `x-cache`          | `STALE`                         | Served stale (stale-if-error)      |\n| `ETag`             | `\"abc123...\"`                   | Content hash for validation        |\n| `Last-Modified`    | `Thu, 01 Jan 2026 00:00:00 GMT` | Last modification date             |\n| `Content-Encoding` | `gzip` or `br`                  | Compression applied                |\n| `Vary`             | `Accept-Encoding`               | Indicates cache varies by encoding |\n\n### Request Headers (for 304)\n\n| Header              | Description                      |\n| ------------------- | -------------------------------- |\n| `If-None-Match`     | Client ETag for validation       |\n| `If-Modified-Since` | Date to check modification       |\n| `Accept-Encoding`   | `gzip, br` to receive compressed |\n\n---\n\n## 💡 Examples\n\n### Serving Static Files\n\n**On your origin server (port 3000):**\n\n```typescript\nimport { AzuraClient } from \"azurajs\";\nimport fs from \"fs\";\nimport path from \"path\";\n\nconst app = new AzuraClient();\n\n// Route to serve files\napp.get(\"/files/:filename\", (req, res) => {\n  const filePath = path.join(\"./uploads\", req.params.filename);\n\n  if (!fs.existsSync(filePath)) {\n    return res.status(404).json({ error: \"File not found\" });\n  }\n\n  // Send with Cache-Control for CDN to respect\n  res.setHeader(\"Cache-Control\", \"public, max-age=3600\"); // 1 hour\n  res.sendFile(filePath);\n});\n\napp.listen(3000);\n```\n\n**Accessing via CDN:**\n\n```bash\n# First request (MISS - fetches from origin)\ncurl http://localhost:3001/files/document.pdf\n# x-cache: MISS\n\n# Second request (HIT - from cache)\ncurl http://localhost:3001/files/document.pdf\n# x-cache: HIT-MEMORY\n```\n\n### Private Content with Signed URL\n\n```typescript\n// On your server (port 3000)\napp.get(\"/private/generate-link/:fileId\", (req, res) => {\n  const signedUrl = cdn.generateSignedUrl(\n    `/private/files/${req.params.fileId}`,\n    300, // 5 minutes\n  );\n\n  res.json({\n    url: `http://localhost:3001${signedUrl}`,\n    expiresIn: 300,\n  });\n});\n\napp.get(\"/private/files/:fileId\", (req, res) => {\n  // This route only accessed if signature is valid\n  const file = getFileById(req.params.fileId);\n  res.sendFile(file.path);\n});\n```\n\n**Usage:**\n\n```bash\n# Generate link\ncurl http://localhost:3000/private/generate-link/123\n# { \"url\": \"http://localhost:3001/private/files/123?expires=...&sig=...\", \"expiresIn\": 300 }\n\n# Access with valid link\ncurl \"http://localhost:3001/private/files/123?expires=1706047200&sig=abc123\"\n# 200 OK\n\n# Access without signature\ncurl http://localhost:3001/private/files/123\n# 403 Forbidden\n```\n\n---\n\n## 📊 Performance\n\n### Typical Benchmarks\n\n| Scenario           | Latency   | Throughput |\n| ------------------ | --------- | ---------- |\n| Cache HIT (memory) | ~1ms      | 50k req/s  |\n| Cache HIT (disk)   | ~5-10ms   | 10k req/s  |\n| Cache MISS         | ~50-200ms | 1k req/s   |\n| With compression   | +2-5ms    | -20% req/s |\n\n### Best Practices\n\n1. **Configure appropriate TTL** - Higher TTL = more hits\n2. **Use Cache-Control on origin** - `public, max-age=3600`\n3. **Enable compression** - Reduces bandwidth by 60-80%\n4. **Use ETag** - Saves bandwidth with 304\n5. **Monitor metrics** - Goal: hit rate > 90%\n\n---\n\n## 🛠️ Troubleshooting\n\n### CDN is not caching\n\n1. Check if origin returns 2xx status\n2. Check if origin returns `Cache-Control: private` or `no-store`\n3. Check if TTL is not too low\n\n### 403 on private URLs\n\n1. Check if signature is correct\n2. Check if not expired\n3. Check if secret is the same\n\n### Rate limit reached\n\n```bash\n# Response\n# 429 Too Many Requests\n# { \"error\": \"Rate limit exceeded\" }\n```\n\nIncrease `max` or `windowMs` in configuration.\n\n---\n\n## 📄 License\n\nMIT\n","readmeFilename":"README.md","_rev":"1-903487e407efb043aaf1ca9a31ffdef3"}