Skip to main content
The Gateway Worker is the main entry point for all API requests. It handles routing, authentication, and request proxying.

Architecture

The worker is a simple request handler that processes incoming requests and routes them to appropriate handlers:
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);
    
    // Handle OPTIONS (CORS preflight)
    if (request.method === 'OPTIONS') {
      return handleOptions();
    }
    
    // Handle authentication endpoints
    if (url.pathname === '/auth/login') {
      return handleLogin(request, env);
    }
    if (url.pathname === '/auth/guest') {
      return handleGuestToken(request, env);
    }
    
    // Handle route proxying
    for (const match of ROUTES) {
      if (url.pathname.startsWith(match.prefix)) {
        return handleProxy(request, env, match);
      }
    }
    
    // 404 for unmatched routes
    return errorResponse(ProjectRoute.CORE, 404, 'NOT_FOUND', 'Route not handled');
  },
};

Environment Interface

interface Env {
  ACCOUNTS: R2Bucket;              // R2 bucket for admin credentials
  GATEWAY_JWT_SECRET: string;      // JWT signing secret
  BUILD_SERVICE_URL: string;       // Build service endpoint
  KV_SERVICE_URL: string;          // KV service endpoint
  CORE_SERVICE_URL: string;        // Core service endpoint
}

Key Functions

handleLogin

Authenticates admin users and issues JWT tokens.
async function handleLogin(request: Request, env: Env) {
  const { username, password } = await request.json();
  
  // Get credentials from R2
  const key = `auth/admins/${username}.json`;
  const recordObject = await env.ACCOUNTS.get(key);
  if (!recordObject) {
    return errorResponse(ProjectRoute.AUTH, 401, 'INVALID_CREDENTIALS', 'Invalid credentials');
  }
  
  const record = JSON.parse(await recordObject.text());
  
  // Verify password hash
  const computedHash = await hashCredential(record.salt, password);
  if (computedHash !== record.hash) {
    return errorResponse(ProjectRoute.AUTH, 401, 'INVALID_CREDENTIALS', 'Invalid credentials');
  }
  
  // Generate JWT token
  const token = await createGatewayToken({
    projectId: record.username || username,
    route: ProjectRoute.AUTH,
    scopes: ['admin'],
    environment: 'production',
    issuedBy: 'gateway.metacogna.ai',
  }, env.GATEWAY_JWT_SECRET);
  
  return jsonResponse({
    success: true,
    token,
    user: { username: record.username, role: record.role || 'admin' }
  });
}

handleGuestToken

Generates temporary guest tokens for unauthenticated access.
async function handleGuestToken(request: Request, env: Env) {
  const body = await request.json();
  const route = normalizeRoute(body.route); // Defaults to BUILD
  
  const token = await createGatewayToken({
    projectId: 'guest',
    route,
    scopes: ['guest'],
    environment: 'staging',
    issuedBy: 'gateway.metacogna.ai',
  }, env.GATEWAY_JWT_SECRET);
  
  return jsonResponse({ success: true, token, route });
}

handleProxy

Proxies requests to downstream services with authentication verification.
async function handleProxy(request: Request, env: Env, match: RouteMatch) {
  const targetBase = env[match.envKey];
  if (!targetBase) {
    return errorResponse(match.route, 502, 'MISSING_TARGET', `Missing target for ${match.prefix}`);
  }
  
  // Verify token if required
  const shouldVerify = match.route === ProjectRoute.BUILD || 
                       request.headers.has('Authorization');
  if (shouldVerify) {
    try {
      await verifyToken(request, env, match.route);
    } catch (err: any) {
      const code = err?.message === 'missing_token' ? 'MISSING_TOKEN' : 'INVALID_TOKEN';
      const status = err?.message === 'missing_token' ? 401 : 403;
      return errorResponse(match.route, status, code, 'Unauthorized request');
    }
  }
  
  // Rewrite URL and forward request
  const url = new URL(request.url);
  const targetUrl = rewriteUrl(url, match, targetBase);
  
  const headers = new Headers(request.headers);
  headers.delete('Authorization');
  headers.set('X-Gateway-Route', match.route);
  
  const proxyReq = new Request(targetUrl.toString(), {
    method: request.method,
    headers,
    body: request.body,
    redirect: 'manual',
  });
  
  return fetch(proxyReq);
}

verifyToken

Verifies JWT tokens and validates route matching.
async function verifyToken(request: Request, env: Env, route: ProjectRoute) {
  const auth = request.headers.get('Authorization');
  if (!auth || !auth.startsWith('Bearer ')) {
    throw new Error('missing_token');
  }
  
  const token = auth.replace('Bearer ', '').trim();
  const { claims } = await verifyGatewayToken(token, env.GATEWAY_JWT_SECRET);
  
  // Validate route matches (CORE and AUTH have elevated privileges)
  if (claims.route !== route && 
      claims.route !== ProjectRoute.CORE && 
      claims.route !== ProjectRoute.AUTH) {
    throw new Error('route_mismatch');
  }
  
  return claims;
}

Helper Functions

Password Hashing

async function hashCredential(salt: string, password: string) {
  const encoder = new TextEncoder();
  const buffer = encoder.encode(`${salt}${password}`);
  const digest = await crypto.subtle.digest('SHA-256', buffer);
  return toBase64(digest);
}

URL Rewriting

function rewriteUrl(url: URL, match: RouteMatch, target: string) {
  const strippedPath = url.pathname.substring(match.prefix.length) || '/';
  const targetUrl = new URL(target);
  targetUrl.pathname = `${targetUrl.pathname.replace(/\/$/, '')}${strippedPath}`;
  targetUrl.search = url.search;
  return targetUrl;
}

Error Responses

All errors follow the unified GatewayErrorSchema:
const errorResponse = (
  route: ProjectRoute, 
  status: number, 
  code: string, 
  message: string, 
  details?: any
) => jsonResponse(
  GatewayErrorSchema.parse({
    route,
    status,
    code,
    message,
    requestId: crypto.randomUUID(),
    timestamp: new Date().toISOString(),
    details,
  }),
  status,
);

CORS Handling

The gateway handles CORS preflight requests:
function handleOptions() {
  return new Response(null, {
    status: 204,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Headers': 'Authorization, Content-Type',
      'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS',
      'Access-Control-Max-Age': '86400',
    },
  });
}

Configuration

wrangler.toml

name = "metacogna-gateway"
main = "src/index.ts"
compatibility_date = "2024-07-01"
workers_dev = false

routes = [
  { pattern = "api.metacogna.ai/*", zone_name = "metacogna.ai" }
]

[vars]
GATEWAY_JWT_SECRET = "changeme"
BUILD_SERVICE_URL = "https://build.metacogna.ai"
KV_SERVICE_URL = "https://kv.metacogna.ai"
CORE_SERVICE_URL = "https://parti.metacogna.ai"

[[r2_buckets]]
binding = "ACCOUNTS"
bucket_name = "metacogna-accounts"

[[services]]
binding = "CORE_SERVICE"
service = "metacogna"

[[services]]
binding = "PORTAL_SERVICE"
service = "metacogna-ai-worker"

Deployment

Deploy the gateway worker:
cd gateway-api/packages/gateway-worker
bun wrangler deploy
Or use the deployment script:
cd gateway-api
./scripts/deploy-gateway.sh