Skip to content

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.

/**
* 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 {
/** WellKnownManifest for fora.json generation. */
manifest: WellKnownManifest;
/** RSL text content for /rsl.txt serving. */
rslTxt: string;
/** 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[];
/** Exchange info endpoint URL for X-Content-Rules header. */
exchangeInfoUrl: string;
/** Exchange API base URL for fetching fora.json and rsl.txt. */
exchangeApiUrl?: string;
/** Serving mode for fora.json and rsl.txt. */
servingMode: "exchange" | "inline" | "kv" | "origin";
/** 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;
}

Runtime: CloudFront Functions (viewer request event) or Lambda@Edge.

Capabilities and limitations:

CapabilityCloudFront FunctionLambda@Edge
Execution time limit1ms (viewer request)5s (viewer request), 30s (origin request)
Memory2 MB128-3008 MB
Network accessNoYes
KV accessCloudFront KeyValueStoreDynamoDB (requires network)
Native signed URL verificationYes (trusted_key_groups on behavior)Yes
Package size10 KB50 MB
LanguagesJavaScript (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, fora.json serving, and signature passthrough. CloudFront’s trusted_key_groups on 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_groups configured on the cache behavior. The CDN itself rejects invalid signatures before your function code runs.
  • Expiry enforcement (the Expires or Policy parameter).
  • Edge caching of content responses.

What you must implement in the function:

  • Bot detection (User-Agent check against botPatterns).
  • fora.json and rsl.txt serving.
  • Custom parameters validation (agent_id binding).
  • Single-use enforcement (requires Lambda@Edge + DynamoDB, best-effort).
  • The 403 response body and X-Content-Rules header.
// CloudFront Function (viewer-request) - ES 5.1 subset
function handler(event) {
var request = event.request;
var uri = request.uri;
var qs = request.querystring;
// Serve fora.json
if (uri === '/.well-known/fora.json') {
return {
statusCode: 200,
statusDescription: 'OK',
headers: {
'content-type': { value: 'application/json' },
'cache-control': { value: 'public, max-age=3600' }
},
body: MANIFEST_JSON
};
}
// Serve rsl.txt
if (uri === '/rsl.txt') {
return {
statusCode: 200,
statusDescription: 'OK',
headers: {
'content-type': { value: 'text/plain' },
'cache-control': { value: 'public, max-age=3600' }
},
body: RSL_TXT
};
}
// 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' },
'x-content-rules': { value: EXCHANGE_INFO_URL }
},
body: '{"error":"Licensed content. Negotiate access via the Exchange.","protocol":"FORA","version":"1.0","info_url":"' + EXCHANGE_INFO_URL + '"}'
};
}
// Regular browser traffic - pass through
return request;
}

Runtime: Cloudflare Workers (V8 isolate, Service Worker or Module Worker syntax).

Capabilities and limitations:

CapabilityCloudflare Worker
Execution time limit10ms CPU (free), 30s (paid)
Memory128 MB
Network accessYes (fetch)
KV accessWorkers KV (eventually consistent, ~60s)
Durable ObjectsYes (strongly consistent, for single-use)
Native signed URL verificationNo (no equivalent to CloudFront trusted_key_groups)
Package size10 MB
LanguagesJavaScript, 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.verify with the Ed25519 algorithm 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 EdgeConfig in Workers KV. Refresh on a timer or via Cron Trigger.

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.

Runtime: Fastly Compute (WebAssembly, compiled from Rust, Go, JavaScript, or AssemblyScript).

Capabilities and limitations:

CapabilityFastly Compute
Execution time limitNo hard limit (billing-based)
Memory128 MB
Network accessYes (backend fetch)
KV accessFastly KV Store (strongly consistent within POP)
Native signed URL verificationNo native equivalent
Package size100 MB (WASM binary)
LanguagesRust (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.

Recommended approach: Single Compute service.

FeatureCloudFront FunctionCloudFront + Lambda@EdgeCloudflare WorkerFastly Compute
Bot detectionYesYesYesYes
fora.json + rsl.txt servingYesYesYesYes
Native signed URL verificationYes (trusted_key_groups)Yes (trusted_key_groups)No (must implement)No (must implement)
Ed25519 signed-URL verificationNo (1ms limit, no Ed25519)YesYes (native Web Crypto)Yes (injected verifier)
Agent identity bindingNo (no network)YesYesYes
Single-use enforcementNo (no KV write)Yes (DynamoDB)Yes (Durable Objects)Yes (KV Store, strong)
Consistency modelN/AStrong (DynamoDB)Strong (Durable Objects)Strong (in-POP)
Latency overhead<0.1ms1-5ms<1ms<1ms
Cost at 1M req/day$0.10$0.70 + DDB$0.50Usage-based