Skip to content

Latest commit

 

History

History
285 lines (223 loc) · 8.91 KB

File metadata and controls

285 lines (223 loc) · 8.91 KB

AGENTS.md

Guidelines for AI agents working in this repository.

Project Overview

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-usd header 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: false to 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

Commands

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 types

Project Structure

config/
  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

TypeScript Configuration

  • Target: ESNext
  • Module: ESNext with Bundler resolution
  • Strict mode enabled
  • JSX: react-jsx with hono/jsx import source

Code Style Guidelines

Imports

// 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;

Naming Conventions

Type Convention Example
Constants SCREAMING_SNAKE CF_ACCESS_TEAM_NAME
Functions camelCase createErrorResponse
Interfaces PascalCase ErrorResponse
Variables camelCase cachedKeys

Type Definitions

// 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[] };

Error Handling

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}`;

Comments

  • 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 = [...]

Formatting

  • Tabs for indentation
  • Double quotes for strings
  • Semicolons at end of statements

Environment Variables

Worker bindings (defined in wrangler.jsonc):

  • GATEWAY_API_KEY - AI Gateway authorization key (secret)
  • GATEWAY_URL - AI Gateway base URL
  • GATEWAY_ACCOUNT_ID - Cloudflare account ID
  • GATEWAY_ID - Gateway identifier
  • CF_ACCESS_TEAM_NAME - Cloudflare Access team name (from Zero Trust dashboard)
  • ASSETS - Static assets binding
  • O4E_TEAM_USAGE - KV namespace for tracking team credit usage
  • O4E_TEAM_CONFIG - KV namespace for team configuration (email mappings, credit limits)
  • O4E_CONFIG_CACHE - KV namespace for caching OpenAI models list

Configuration

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.

Key Patterns

Caching (JWKS example)

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 Credit Management

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:

  1. Extract email from validated JWT
  2. Look up team via email → teamId mapping in TEAM_CONFIG
  3. Check current usage from TEAM_USAGE
  4. If totalCost >= creditLimit, return 429 (quota exceeded)
  5. Proxy request to AI Gateway
  6. Parse usage cost from response metadata
  7. Update TEAM_USAGE with incremented cost

Request Proxying

  • Strip incoming Authorization and x-api-key headers
  • Add cf-aig-authorization with GATEWAY_API_KEY
  • Add cf-aig-metadata with user context (email, teamId)
  • Return streaming response (pass through from AI Gateway)
  • Extract usage cost from cf-aig-cost-usd response header (defaults to $0.001 if not present)
  • Update team usage in KV after each request

OpenAI Models Management

The worker dynamically manages OpenAI model configurations:

  1. Scheduled cron job (hourly): Fetches latest OpenAI models from models.dev API
  2. Caching: Stores model list in O4E_CONFIG_CACHE KV with 24-hour expiration
  3. Filtering: Excludes embedding models (e.g., text-embedding-*)
  4. ZDR Compliance: Applies store: false to all OpenAI models in the config
  5. Fallback: Uses hardcoded model list if fetch fails or KV is empty

Special handling for gpt-5.2:

  • Includes reasoning.encrypted_content in response
  • Supports variants: none, low, medium, high, xhigh

Authentication Flow

  1. User makes request with cf-access-token header (from Cloudflare Access login)
  2. Worker fetches JWKS public keys from https://{CF_ACCESS_TEAM_NAME}.cloudflareaccess.com/cdn-cgi/access/certs
  3. JWKS keys are cached in-memory for 1 hour
  4. JWT token is cryptographically verified using Jwt.verifyWithJwks
  5. Email is extracted from JWT payload
  6. Worker looks up team by iterating through all team configs in KV to find matching email
  7. 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.

Retry Logic

Critical operations use exponential backoff retry:

withRetry(fn, delays = [5000, 10000]) // 3 attempts total

Applied to:

  • JWKS public key fetching

Development Guidelines

  • Minimize introducing dependencies unless necessary
  • Install dependencies using project toolchain (npm)
  • Do NOT commit/push without explicit instruction