Guidelines for AI agents working in this repository.
Cloudflare Worker that provides secure, budget-controlled OpenCode access for event participants through Cloudflare Access authentication and team-based credit limits.
What it does:
- Authenticates users via Cloudflare Access (JWT with JWKS verification)
- Maps authenticated user emails to teams from KV storage
- Enforces per-team credit limits with real-time usage tracking
- Proxies requests to Cloudflare AI Gateway (Anthropic & OpenAI providers)
- Tracks costs via
cf-aig-cost-usdheader and updates team usage in KV - Blocks access with 429 when team exceeds credit limit
- Serves dynamic OpenCode configuration at
/.well-known/opencode - Caches OpenAI models list from models.dev (refreshed hourly via cron)
- Applies
store: falseto all OpenAI models for ZDR compatibility
Use Case: Hackathons, workshops, and coding events where participants are organized into teams with fixed credit allocations (e.g., $20 per team). Access is automatically denied when credits are exhausted.
Tech Stack: Cloudflare Workers, Hono, TypeScript, Wrangler, KV Storage, Cloudflare Access, AI Gateway
npm install # Install dependencies
npm run build:config # Build OpenCode config (runs automatically)
npm run setup-teams # Upload team configurations
npm run view-teams # View team usage
npm run dev # Development server (wrangler dev)
npm run deploy # Production deployment
npm run cf-typegen # Generate Cloudflare typesconfig/
base.json # Core config (providers and settings)
opencode.json # Generated - DO NOT EDIT (gitignored)
public/
index.html # Landing page
scripts/
build-config.ts # Compiles config into opencode.json
setup-teams.ts # Bulk upload team configurations
view-teams.ts # View team usage and status
src/
index.ts # Main Hono app entry point
wrangler.jsonc # Wrangler config
teams.example.json # Example team configuration
- Target: ESNext
- Module: ESNext with Bundler resolution
- Strict mode enabled
- JSX: react-jsx with hono/jsx import source
// Named imports from libraries
import { Hono } from "hono";
// Type-only imports (use 'import type' for types)
import type { JwtVariables } from "hono/jwt";
import type { HonoJsonWebKey } from "hono/utils/jwt/jws";
// Local imports
import configTemplate from "../config/opencode.json";
// Default exports for app/tool modules
export default app;| Type | Convention | Example |
|---|---|---|
| Constants | SCREAMING_SNAKE | CF_ACCESS_TEAM_NAME |
| Functions | camelCase | createErrorResponse |
| Interfaces | PascalCase | ErrorResponse |
| Variables | camelCase | cachedKeys |
// Define interfaces for data structures
interface Env {
GATEWAY_API_KEY: string;
GATEWAY_URL: string;
}
interface ErrorResponse {
error: string;
message: string;
status?: number;
timestamp: string;
}
// Type Hono apps with bindings and variables
type Variables = JwtVariables<{ email?: string }>;
const app = new Hono<{ Bindings: Env; Variables: Variables }>();
// Type JSON responses explicitly
const data = await response.json() as { keys: HonoJsonWebKey[] };Use structured error responses with consistent format:
interface ErrorResponse {
error: string; // Error type (e.g., "Unauthorized", "Gateway Error")
message: string; // Human-readable message
status?: number; // HTTP status code
timestamp: string; // ISO timestamp
}
function createErrorResponse(message: string, status = 500, error = 'Internal Server Error'): Response {
const errorResponse: ErrorResponse = {
error,
message,
status,
timestamp: new Date().toISOString()
};
console.log(errorResponse); // Always log errors
return new Response(JSON.stringify(errorResponse), {
status,
headers: { 'Content-Type': 'application/json' }
});
}For tools, return error strings (don't throw):
return `Error: ${message}`;- Focus on "why", not "what"
- Don't comment single variables or short functions
- Comment logic with I/O, validation, or edge cases
// Good: explains why
// 1 hour TTL for JWKS cache
cacheExpiration = now + 3600 * 1000;
// Good: documents configurable behavior
// JIRA fields to extract from API response
// Add/remove field paths here to control what's included
const JIRA_INCLUDED_FIELDS = [...]- Tabs for indentation
- Double quotes for strings
- Semicolons at end of statements
Worker bindings (defined in wrangler.jsonc):
GATEWAY_API_KEY- AI Gateway authorization key (secret)GATEWAY_URL- AI Gateway base URLGATEWAY_ACCOUNT_ID- Cloudflare account IDGATEWAY_ID- Gateway identifierCF_ACCESS_TEAM_NAME- Cloudflare Access team name (from Zero Trust dashboard)ASSETS- Static assets bindingO4E_TEAM_USAGE- KV namespace for tracking team credit usageO4E_TEAM_CONFIG- KV namespace for team configuration (email mappings, credit limits)O4E_CONFIG_CACHE- KV namespace for caching OpenAI models list
OpenCode configuration is managed in config/base.json and compiled by npm run build:config.
The generated config/opencode.json is gitignored - do not edit directly.
Changes are served at /.well-known/opencode.
let cachedKeys: HonoJsonWebKey[] | null = null;
let cacheExpiration = 0;
async function getPublicKeys(): Promise<HonoJsonWebKey[]> {
const now = Date.now();
if (cachedKeys && now < cacheExpiration) {
return cachedKeys;
}
// Fetch and cache...
cacheExpiration = now + 3600 * 1000; // 1 hour
return cachedKeys;
}Team data structure stored in TEAM_CONFIG KV:
interface TeamConfig {
teamId: string;
emails: string[]; // List of participant emails
creditLimit: number; // Maximum credits in USD (e.g., 20.00)
}Usage tracking stored in TEAM_USAGE KV:
interface TeamUsage {
teamId: string;
totalCost: number; // Accumulated cost in USD
lastUpdated: string; // ISO timestamp
requestCount: number; // Total requests made
}Flow:
- Extract email from validated JWT
- Look up team via email → teamId mapping in
TEAM_CONFIG - Check current usage from
TEAM_USAGE - If
totalCost >= creditLimit, return 429 (quota exceeded) - Proxy request to AI Gateway
- Parse usage cost from response metadata
- Update
TEAM_USAGEwith incremented cost
- Strip incoming
Authorizationandx-api-keyheaders - Add
cf-aig-authorizationwith GATEWAY_API_KEY - Add
cf-aig-metadatawith user context (email, teamId) - Return streaming response (pass through from AI Gateway)
- Extract usage cost from
cf-aig-cost-usdresponse header (defaults to $0.001 if not present) - Update team usage in KV after each request
The worker dynamically manages OpenAI model configurations:
- Scheduled cron job (hourly): Fetches latest OpenAI models from models.dev API
- Caching: Stores model list in
O4E_CONFIG_CACHEKV with 24-hour expiration - Filtering: Excludes embedding models (e.g.,
text-embedding-*) - ZDR Compliance: Applies
store: falseto all OpenAI models in the config - Fallback: Uses hardcoded model list if fetch fails or KV is empty
Special handling for gpt-5.2:
- Includes
reasoning.encrypted_contentin response - Supports variants: none, low, medium, high, xhigh
- User makes request with
cf-access-tokenheader (from Cloudflare Access login) - Worker fetches JWKS public keys from
https://{CF_ACCESS_TEAM_NAME}.cloudflareaccess.com/cdn-cgi/access/certs - JWKS keys are cached in-memory for 1 hour
- JWT token is cryptographically verified using
Jwt.verifyWithJwks - Email is extracted from JWT payload
- Worker looks up team by iterating through all team configs in KV to find matching email
- If no team found, return 403 Forbidden
Note: CF_ACCESS_TEAM_NAME is configured in wrangler.jsonc and should match your Cloudflare Access team name from the Zero Trust dashboard.
Critical operations use exponential backoff retry:
withRetry(fn, delays = [5000, 10000]) // 3 attempts totalApplied to:
- JWKS public key fetching
- Minimize introducing dependencies unless necessary
- Install dependencies using project toolchain (npm)
- Do NOT commit/push without explicit instruction