CDN Adapters
The Edge Function is defined as a platform-agnostic interface. Each CDN platform gets a thin adapter that maps platform primitives to the interface. The core decision logic is CDN-agnostic — every adapter calls the same core function.
Generic Adapter Interface
Section titled “Generic Adapter Interface”/** * Platform-agnostic Edge Function interface. * Each CDN adapter implements this contract. */interface ForaEdgeHandler { /** * Inspect an incoming request and decide: block, pass, or serve. */ handleRequest(ctx: EdgeContext): Promise<EdgeResponse>;}
interface EdgeContext { /** Incoming request URL (full, including query params). */ url: URL;
/** HTTP method. */ method: string;
/** Request headers (case-insensitive lookup). */ headers: ReadonlyHeaders;
/** Client IP address. */ clientIp: string;
/** Provider configuration loaded from config source. */ config: EdgeConfig;
/** Edge KV store (optional, for single-use enforcement). */ kv?: EdgeKV;}
interface EdgeResponse { status: number; headers: Record<string, string>; body: string | ReadableStream | null;}
interface EdgeKV { /** Atomic put-if-absent. Returns true if key was inserted (first use). */ putIfAbsent(key: string, value: string, ttlSeconds: number): Promise<boolean>; /** Read a key. */ get(key: string): Promise<string | null>;}
interface EdgeConfig { /** Glob patterns that require a license (carry FORA terms); all other paths * are open. Compiled from RSL/catalog entries — a local edge type, not a wire * message. */ protectedPatterns: string[];
/** Value of the X-Content-Rules header on a 403: the absolute URL of this * provider's own manifest, https://{provider domain}/.well-known/fora.json. */ contentRulesUrl: string;
/** Optional value of the X-FORA-Exchange header on a 403: the bare domain of * an Exchange the manifest lists (e.g. "exchange.example"), never a URL. */ exchangeDomain?: string;
/** Bot User-Agent patterns to detect (regexes). */ botPatterns: RegExp[];
/** Exchange key directory the edge resolves `kid` against. */ exchangeWbaUrl: string;
/** Optional pinned public keys; a covering `kid` skips the directory fetch. */ verifyKeys?: JsonWebKey[];
/** Whether to enforce single-use URLs (best-effort). Requires kv. */ singleUseEnabled: boolean;
/** Whether to enforce agent identity binding. Defaults to true. */ enforceBinding: boolean;}CloudFront Functions (AWS)
Section titled “CloudFront Functions (AWS)”Runtime: CloudFront Functions (viewer request event) or Lambda@Edge.
Capabilities and limitations:
| Capability | CloudFront Function | Lambda@Edge |
|---|---|---|
| Execution time limit | 1ms (viewer request) | 5s (viewer request), 30s (origin request) |
| Memory | 2 MB | 128-3008 MB |
| Network access | No | Yes |
| KV access | CloudFront KeyValueStore | DynamoDB (requires network) |
| Native signed URL verification | Yes (trusted_key_groups on behavior) | Yes |
| Package size | 10 KB | 50 MB |
| Languages | JavaScript (ES 5.1 subset) | Node.js, Python |
| Cost | $0.10/million invocations | $0.60/million + duration |
Recommended approach: Two-layer deployment.
- CloudFront Function handles bot detection and signature passthrough, and hands
/.well-known/fora.jsonand/rsl.txtback to CloudFront, which fetches them from the origin. CloudFront’strusted_key_groupson the cache behavior natively verifies RSA-signed URLs — no custom code needed. - Lambda@Edge (optional) handles agent identity binding and single-use enforcement. These require network access (DynamoDB lookup) which CloudFront Functions cannot do.
What CloudFront does natively that you do not reimplement:
- RSA signature verification on signed URLs via
trusted_key_groupsconfigured on the cache behavior. The CDN itself rejects invalid signatures before your function code runs. - Expiry enforcement (the
ExpiresorPolicyparameter). - Edge caching of content responses.
What you must implement in the function:
- Bot detection (User-Agent check against
botPatterns). - Handing
/.well-known/fora.jsonand/rsl.txtto the origin, ahead of bot detection. - Custom parameters validation (agent_id binding).
- Single-use enforcement (requires Lambda@Edge + DynamoDB, best-effort).
- The 403 response body and the
X-Content-RulesandX-FORA-Exchangeheaders.
// CloudFront Function (viewer-request) - ES 5.1 subsetfunction handler(event) { var request = event.request; var uri = request.uri; var qs = request.querystring;
// The provider's own files: the origin answers these, for every client if (uri === '/.well-known/fora.json' || uri === '/rsl.txt') { return request; // pass through }
// Only process protected paths if (!uri.startsWith('/premium/')) { return request; // pass through }
// If signed URL params present, let CloudFront trusted_key_groups handle verification if (qs.Signature || qs['Key-Pair-Id']) { return request; // pass through to native verification }
// Bot detection var ua = (request.headers['user-agent'] || {}).value || ''; if (isAiBot(ua)) { return { statusCode: 403, statusDescription: 'Forbidden', headers: { 'content-type': { value: 'application/json' }, // The provider's own manifest, e.g. https://publisher.example/.well-known/fora.json 'x-content-rules': { value: CONTENT_RULES_URL }, // A bare domain the manifest lists, e.g. exchange.example 'x-fora-exchange': { value: EXCHANGE_DOMAIN } }, body: '{"error":"Licensed content. Negotiate access via the Exchange.","protocol":"FORA","version":"1.0","fora_json_url":"' + CONTENT_RULES_URL + '"}' }; }
// Regular browser traffic - pass through return request;}Cloudflare Workers
Section titled “Cloudflare Workers”Runtime: Cloudflare Workers (V8 isolate, Service Worker or Module Worker syntax).
Capabilities and limitations:
| Capability | Cloudflare Worker |
|---|---|
| Execution time limit | 10ms CPU (free), 30s (paid) |
| Memory | 128 MB |
| Network access | Yes (fetch) |
| KV access | Workers KV (eventually consistent, ~60s) |
| Durable Objects | Yes (strongly consistent, for single-use) |
| Native signed URL verification | No (no equivalent to CloudFront trusted_key_groups) |
| Package size | 10 MB |
| Languages | JavaScript, TypeScript, Rust (WASM) |
Key difference from CloudFront: Cloudflare has no native signed URL verification. The Worker verifies the Ed25519 signature in code, using only the Exchange’s public key. Nothing secret is deployed to the edge.
Recommended approach: Single Worker with optional Durable Objects.
- Signature verification: Must be done in Worker code. Use
crypto.subtle.verifywith theEd25519algorithm and the imported public key. Workers support Ed25519 natively; Fastly does not, which is why the SDK’s verifier accepts an injected implementation. - Single-use enforcement: Workers KV is eventually consistent (up to 60 seconds propagation). For strict single-use, use Durable Objects which provide transactional consistency. For soft single-use (acceptable eventual consistency), Workers KV with a short TTL is cheaper.
- Config: Store
EdgeConfigin Workers KV. Refresh on a timer or via Cron Trigger.
Akamai
Section titled “Akamai”Akamai is not a supported target. Its EdgeAuth token scheme verifies with a secret shared with the CDN, which is the model FORA deliberately does not use: every FORA delivery endpoint verifies with a public key and holds nothing secret. Running FORA on Akamai would mean writing the Ed25519 verifier as an EdgeWorker, which the platform’s 4 ms budget and 2 MB handler memory make an open question rather than a supported path.
Fastly Compute@Edge
Section titled “Fastly Compute@Edge”Runtime: Fastly Compute (WebAssembly, compiled from Rust, Go, JavaScript, or AssemblyScript).
Capabilities and limitations:
| Capability | Fastly Compute |
|---|---|
| Execution time limit | No hard limit (billing-based) |
| Memory | 128 MB |
| Network access | Yes (backend fetch) |
| KV access | Fastly KV Store (strongly consistent within POP) |
| Native signed URL verification | No native equivalent |
| Package size | 100 MB (WASM binary) |
| Languages | Rust (primary), JavaScript/TypeScript, Go |
Key advantages:
- No execution time limit (billed per request + compute duration). Complex verification logic is fine.
- Fastly KV Store is strongly consistent within a POP, making single-use enforcement reliable without the eventual-consistency problems of Workers KV or EdgeKV.
- Full WASM runtime means you can compile the same Rust/Go verification logic that runs in the Exchange.
With the TypeScript SDK: Fastly’s JavaScript runtime has no WebCrypto Ed25519 and no AbortController. Pass verifyEd25519 to the SDK’s verifiers and directory readers; its document reads need no AbortController, and still end at their 30-second deadline.
Recommended approach: Single Compute service.
Platform Comparison Matrix
Section titled “Platform Comparison Matrix”| Feature | CloudFront Function | CloudFront + Lambda@Edge | Cloudflare Worker | Fastly Compute |
|---|---|---|---|---|
| Bot detection | Yes | Yes | Yes | Yes |
| fora.json + rsl.txt passthrough | Yes | Yes | Yes | Yes |
| Native signed URL verification | Yes (trusted_key_groups) | Yes (trusted_key_groups) | No (must implement) | No (must implement) |
| Ed25519 signed-URL verification | No (1ms limit, no Ed25519) | Yes | Yes (native Web Crypto) | Yes (injected verifier) |
| Agent identity binding | No (no network) | Yes | Yes | Yes |
| Single-use enforcement | No (no KV write) | Yes (DynamoDB) | Yes (Durable Objects) | Yes (KV Store, strong) |
| Consistency model | N/A | Strong (DynamoDB) | Strong (Durable Objects) | Strong (in-POP) |
| Latency overhead | <0.1ms | 1-5ms | <1ms | <1ms |
| Cost at 1M req/day | $0.10 | $0.70 + DDB | $0.50 | Usage-based |