branch:
server.ts
5315 bytesRaw
/**
* Worker entry point — routes requests to:
* 1. /api/token → issue a JWT (simulates your existing auth system)
* 2. /agents/* → routeAgentRequest() with JWT middleware
* 3. /* → Vite SPA (via wrangler assets config)
*/
import { AIChatAgent, type OnChatMessageOptions } from "@cloudflare/ai-chat";
import { createWorkersAI } from "workers-ai-provider";
import { streamText, convertToModelMessages } from "ai";
import { routeAgentRequest } from "agents";
import { SignJWT, jwtVerify } from "jose";
// ── JWT helpers ──────────────────────────────────────────────────────────────
function getSecret(env: Env) {
if (!env.AUTH_SECRET) {
throw new Error(
'AUTH_SECRET is not set. Run: echo "AUTH_SECRET=$(openssl rand -base64 32)" > .env'
);
}
return new TextEncoder().encode(env.AUTH_SECRET);
}
/** Issue a short-lived JWT containing the user's name. */
async function issueToken(env: Env, name: string) {
return new SignJWT({ sub: name })
.setProtectedHeader({ alg: "HS256" })
.setIssuer("auth-agent")
.setAudience("auth-agent")
.setIssuedAt()
.setExpirationTime("1h")
.sign(getSecret(env));
}
/** Verify a JWT and return its payload, or null if invalid/expired. */
async function verifyToken(env: Env, token: string) {
try {
const { payload } = await jwtVerify(token, getSecret(env), {
issuer: "auth-agent",
audience: "auth-agent"
});
return payload;
} catch {
return null;
}
}
// ── Agent ────────────────────────────────────────────────────────────────────
export class ChatAgent extends AIChatAgent<Env> {
async onChatMessage(_onFinish: unknown, options?: OnChatMessageOptions) {
const workersai = createWorkersAI({ binding: this.env.AI });
// this.name comes from the Durable Object name, which is set to the user's
// name from the JWT sub claim. Note: in production, sanitise user-controlled
// values before interpolating into prompts to mitigate prompt injection.
const userName = this.name;
const result = streamText({
abortSignal: options?.abortSignal,
model: workersai("@cf/moonshotai/kimi-k2.5", {
sessionAffinity: this.sessionAffinity
}),
system: `You are a helpful assistant. The user's name is ${userName}. Address them by name occasionally.`,
messages: await convertToModelMessages(this.messages)
});
return result.toUIMessageStreamResponse();
}
}
// ── Worker fetch handler ─────────────────────────────────────────────────────
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// ⚠️ DEMO ONLY — this endpoint issues JWTs to anyone without authentication.
// In production, replace this with your own auth service / identity provider.
if (url.pathname === "/api/token") {
if (request.method !== "POST") {
return Response.json({ error: "Method not allowed" }, { status: 405 });
}
let body: { name?: string };
try {
body = (await request.json()) as { name?: string };
} catch {
return Response.json({ error: "Invalid JSON" }, { status: 400 });
}
const name = body.name?.trim();
if (!name) {
return Response.json({ error: "Name is required" }, { status: 400 });
}
const token = await issueToken(env, name);
return Response.json({ token });
}
// Agent routes — protected by JWT
if (url.pathname.startsWith("/agents")) {
const response = await routeAgentRequest(request, env, {
// WebSocket: JWT passed as ?token= query param
onBeforeConnect: async (req) => {
const token = new URL(req.url).searchParams.get("token");
if (!token)
return Response.json({ error: "Missing token" }, { status: 401 });
const payload = await verifyToken(env, token);
if (!payload)
return Response.json({ error: "Unauthorized" }, { status: 401 });
return req;
},
// HTTP: JWT from Authorization header or ?token= query param
onBeforeRequest: async (req) => {
const authHeader = req.headers.get("Authorization");
const token = authHeader?.startsWith("Bearer ")
? authHeader.slice(7)
: new URL(req.url).searchParams.get("token");
if (!token)
return Response.json({ error: "Missing token" }, { status: 401 });
const payload = await verifyToken(env, token);
if (!payload)
return Response.json({ error: "Unauthorized" }, { status: 401 });
return req;
}
});
if (response) return response;
return new Response("Agent not found", { status: 404 });
}
// SPA fallback (handled by wrangler assets config)
return new Response("Not found", { status: 404 });
}
} satisfies ExportedHandler<Env>;