import { Hono, type Context, type MiddlewareHandler } from "hono" import { cors } from "hono/cors" import { eq } from "drizzle-orm" import { browser_session_tabs, browser_sessions, canvas, canvas_images, chat_messages, chat_threads, context_items, thread_context_items, } from "../../web/src/db/schema" import { getDb, type Hyperdrive } from "./db" type Env = { ADMIN_API_KEY?: string DATABASE_URL?: string HYPERDRIVE?: Hyperdrive } // Create a new Hono app type AppEnv = { Bindings: Env } const app = new Hono() // Enable CORS for all routes app.use( "/*", cors({ origin: "*", allowHeaders: ["Authorization", "Content-Type"], allowMethods: ["GET", "POST", "PATCH", "DELETE", "OPTIONS"], }), ) const requireAdmin: MiddlewareHandler = async (c, next) => { if (c.req.method === "OPTIONS") { return next() } const apiKey = c.env.ADMIN_API_KEY if (!apiKey) { return next() } const authHeader = c.req.header("Authorization") if (!authHeader || !authHeader.startsWith("Bearer ")) { return c.json({ error: "Missing Authorization header" }, 401) } const providedKey = authHeader.slice(7) if (providedKey !== apiKey) { return c.json({ error: "Invalid API key" }, 401) } return next() } app.use("/api/v1/admin/*", requireAdmin) const parseBody = async (c: Context) => { return (await c.req.json().catch(() => ({}))) as Record } const parseInteger = (value: unknown) => { const numberValue = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN return Number.isInteger(numberValue) ? numberValue : null } const parseDate = (value: unknown) => { if (typeof value !== "string" && typeof value !== "number") { return null } const date = new Date(value) return Number.isNaN(date.getTime()) ? null : date } const parsePosition = (value: unknown) => { if ( value && typeof value === "object" && "x" in value && "y" in value && typeof (value as { x: unknown }).x === "number" && typeof (value as { y: unknown }).y === "number" ) { return { x: (value as { x: number }).x, y: (value as { y: number }).y } } return null } const parseSize = (value: unknown) => { if ( value && typeof value === "object" && "width" in value && "height" in value && typeof (value as { width: unknown }).width === "number" && typeof (value as { height: unknown }).height === "number" ) { return { width: (value as { width: number }).width, height: (value as { height: number }).height, } } return null } // Health check endpoint app.get("/health", (c) => { return c.json({ status: "ok", message: "Worker is running!" }) }) // Root endpoint app.get("/", (c) => { return c.json({ message: "Welcome to the Cloudflare Worker API", endpoints: { health: "/health", api: "/api/v1", admin: "/api/v1/admin", }, }) }) // Example API endpoint app.get("/api/v1/hello", (c) => { const name = c.req.query("name") || "World" return c.json({ message: `Hello, ${name}!` }) }) // Canvas endpoints app.post("/api/v1/admin/canvas", async (c) => { const body = await parseBody(c) const ownerId = typeof body.ownerId === "string" ? body.ownerId.trim() : "" if (!ownerId) { return c.json({ error: "ownerId required" }, 400) } const values: typeof canvas.$inferInsert = { owner_id: ownerId, } if (typeof body.name === "string" && body.name.trim()) { values.name = body.name.trim() } if (typeof body.width === "number" && Number.isFinite(body.width)) { values.width = body.width } if (typeof body.height === "number" && Number.isFinite(body.height)) { values.height = body.height } if (typeof body.defaultModel === "string") { values.default_model = body.defaultModel } if (typeof body.defaultStyle === "string") { values.default_style = body.defaultStyle } if (body.backgroundPrompt === null) { values.background_prompt = null } else if (typeof body.backgroundPrompt === "string") { values.background_prompt = body.backgroundPrompt } try { const database = getDb(c.env) const [record] = await database.insert(canvas).values(values).returning() return c.json({ canvas: record }, 201) } catch (error) { console.error("[worker] create canvas failed", error) return c.json({ error: "Failed to create canvas" }, 500) } }) app.patch("/api/v1/admin/canvas/:canvasId", async (c) => { const canvasId = c.req.param("canvasId") if (!canvasId) { return c.json({ error: "canvasId required" }, 400) } const body = await parseBody(c) const updates: Partial = { updated_at: new Date(), } if (typeof body.name === "string") { updates.name = body.name } if (typeof body.width === "number" && Number.isFinite(body.width)) { updates.width = body.width } if (typeof body.height === "number" && Number.isFinite(body.height)) { updates.height = body.height } if (typeof body.defaultModel === "string") { updates.default_model = body.defaultModel } if (typeof body.defaultStyle === "string") { updates.default_style = body.defaultStyle } if (body.backgroundPrompt === null) { updates.background_prompt = null } else if (typeof body.backgroundPrompt === "string") { updates.background_prompt = body.backgroundPrompt } if (Object.keys(updates).length <= 1) { return c.json({ error: "No updates provided" }, 400) } try { const database = getDb(c.env) const [record] = await database .update(canvas) .set(updates) .where(eq(canvas.id, canvasId)) .returning() if (!record) { return c.json({ error: "Canvas not found" }, 404) } return c.json({ canvas: record }) } catch (error) { console.error("[worker] update canvas failed", error) return c.json({ error: "Failed to update canvas" }, 500) } }) app.post("/api/v1/admin/canvas/:canvasId/images", async (c) => { const canvasId = c.req.param("canvasId") if (!canvasId) { return c.json({ error: "canvasId required" }, 400) } const body = await parseBody(c) const position = parsePosition(body.position) const size = parseSize(body.size) const values: typeof canvas_images.$inferInsert = { canvas_id: canvasId, } if (typeof body.name === "string") values.name = body.name if (typeof body.prompt === "string") values.prompt = body.prompt if (typeof body.modelId === "string") values.model_id = body.modelId if (typeof body.modelUsed === "string") values.model_used = body.modelUsed if (typeof body.styleId === "string") values.style_id = body.styleId if (typeof body.rotation === "number" && Number.isFinite(body.rotation)) { values.rotation = body.rotation } if (position) values.position = position if (size) { values.width = size.width values.height = size.height } if (body.metadata !== undefined) { values.metadata = body.metadata && typeof body.metadata === "object" ? body.metadata : null } if (body.branchParentId === null) { values.branch_parent_id = null } else if (typeof body.branchParentId === "string") { values.branch_parent_id = body.branchParentId } if (body.contentBase64 === null) { values.content_base64 = null } else if (typeof body.contentBase64 === "string") { values.content_base64 = body.contentBase64 } else if (typeof body.content_base64 === "string") { values.content_base64 = body.content_base64 } if (body.imageUrl === null) { values.image_url = null } else if (typeof body.imageUrl === "string") { values.image_url = body.imageUrl } try { const database = getDb(c.env) const [image] = await database.insert(canvas_images).values(values).returning() return c.json({ image }, 201) } catch (error) { console.error("[worker] create canvas image failed", error) return c.json({ error: "Failed to create canvas image" }, 500) } }) app.patch("/api/v1/admin/canvas/images/:imageId", async (c) => { const imageId = c.req.param("imageId") if (!imageId) { return c.json({ error: "imageId required" }, 400) } const body = await parseBody(c) const updates: Partial = { updated_at: new Date(), } const position = parsePosition(body.position) const size = parseSize(body.size) if (typeof body.name === "string") updates.name = body.name if (typeof body.prompt === "string") updates.prompt = body.prompt if (typeof body.modelId === "string") updates.model_id = body.modelId if (typeof body.modelUsed === "string") updates.model_used = body.modelUsed if (typeof body.styleId === "string") updates.style_id = body.styleId if (typeof body.rotation === "number" && Number.isFinite(body.rotation)) { updates.rotation = body.rotation } if (position) updates.position = position if (size) { updates.width = size.width updates.height = size.height } if (body.metadata !== undefined) { updates.metadata = body.metadata && typeof body.metadata === "object" ? body.metadata : null } if (body.branchParentId === null) { updates.branch_parent_id = null } else if (typeof body.branchParentId === "string") { updates.branch_parent_id = body.branchParentId } if (body.contentBase64 === null) { updates.content_base64 = null } else if (typeof body.contentBase64 === "string") { updates.content_base64 = body.contentBase64 } else if (typeof body.content_base64 === "string") { updates.content_base64 = body.content_base64 } if (body.imageUrl === null) { updates.image_url = null } else if (typeof body.imageUrl === "string") { updates.image_url = body.imageUrl } if (Object.keys(updates).length <= 1) { return c.json({ error: "No updates provided" }, 400) } try { const database = getDb(c.env) const [image] = await database .update(canvas_images) .set(updates) .where(eq(canvas_images.id, imageId)) .returning() if (!image) { return c.json({ error: "Image not found" }, 404) } return c.json({ image }) } catch (error) { console.error("[worker] update canvas image failed", error) return c.json({ error: "Failed to update canvas image" }, 500) } }) app.delete("/api/v1/admin/canvas/images/:imageId", async (c) => { const imageId = c.req.param("imageId") if (!imageId) { return c.json({ error: "imageId required" }, 400) } try { const database = getDb(c.env) const [deleted] = await database .delete(canvas_images) .where(eq(canvas_images.id, imageId)) .returning() if (!deleted) { return c.json({ error: "Image not found" }, 404) } return c.json({ id: imageId }) } catch (error) { console.error("[worker] delete canvas image failed", error) return c.json({ error: "Failed to delete canvas image" }, 500) } }) // Chat endpoints app.post("/api/v1/admin/chat/threads", async (c) => { const body = await parseBody(c) const title = typeof body.title === "string" && body.title.trim() ? body.title.trim() : "New chat" const userId = typeof body.userId === "string" ? body.userId : null try { const database = getDb(c.env) const [thread] = await database .insert(chat_threads) .values({ title, user_id: userId }) .returning() return c.json({ thread }, 201) } catch (error) { console.error("[worker] create thread failed", error) return c.json({ error: "Failed to create thread" }, 500) } }) app.patch("/api/v1/admin/chat/threads/:threadId", async (c) => { const threadId = parseInteger(c.req.param("threadId")) if (!threadId) { return c.json({ error: "threadId required" }, 400) } const body = await parseBody(c) const title = typeof body.title === "string" ? body.title.trim() : "" if (!title) { return c.json({ error: "title required" }, 400) } try { const database = getDb(c.env) const [thread] = await database .update(chat_threads) .set({ title }) .where(eq(chat_threads.id, threadId)) .returning() if (!thread) { return c.json({ error: "Thread not found" }, 404) } return c.json({ thread }) } catch (error) { console.error("[worker] update thread failed", error) return c.json({ error: "Failed to update thread" }, 500) } }) app.post("/api/v1/admin/chat/messages", async (c) => { const body = await parseBody(c) const threadId = parseInteger(body.threadId) const role = typeof body.role === "string" ? body.role.trim() : "" const content = typeof body.content === "string" ? body.content.trim() : "" const createdAt = parseDate(body.createdAt) if (!threadId || !role || !content) { return c.json({ error: "threadId, role, and content are required" }, 400) } try { const database = getDb(c.env) const values: typeof chat_messages.$inferInsert = { thread_id: threadId, role, content, } if (createdAt) { values.created_at = createdAt } const [message] = await database .insert(chat_messages) .values(values) .returning() return c.json({ message }, 201) } catch (error) { console.error("[worker] add message failed", error) return c.json({ error: "Failed to add message" }, 500) } }) // Context items endpoints app.post("/api/v1/admin/context-items", async (c) => { const body = await parseBody(c) const userId = typeof body.userId === "string" ? body.userId.trim() : "" const type = typeof body.type === "string" ? body.type.trim().toLowerCase() : "" const url = typeof body.url === "string" ? body.url.trim() : null const name = typeof body.name === "string" && body.name.trim() ? body.name.trim() : url ? (() => { try { const parsed = new URL(url) return `${parsed.hostname}${parsed.pathname}` } catch { return url } })() : "Untitled context" const threadId = parseInteger(body.threadId) const parentId = parseInteger(body.parentId) if (!userId) { return c.json({ error: "userId required" }, 400) } if (type !== "url" && type !== "file") { return c.json({ error: "type must be 'url' or 'file'" }, 400) } const values: typeof context_items.$inferInsert = { user_id: userId, type, name, } if (url) values.url = url if (body.content === null) { values.content = null } else if (typeof body.content === "string") { values.content = body.content } if (typeof body.refreshing === "boolean") { values.refreshing = body.refreshing } if (parentId) { values.parent_id = parentId } try { const database = getDb(c.env) const [item] = await database.insert(context_items).values(values).returning() if (threadId) { await database.insert(thread_context_items).values({ thread_id: threadId, context_item_id: item.id, }) } return c.json({ item }, 201) } catch (error) { console.error("[worker] create context item failed", error) return c.json({ error: "Failed to create context item" }, 500) } }) app.patch("/api/v1/admin/context-items/:itemId", async (c) => { const itemId = parseInteger(c.req.param("itemId")) if (!itemId) { return c.json({ error: "itemId required" }, 400) } const body = await parseBody(c) const updates: Partial = { updated_at: new Date(), } const parentId = parseInteger(body.parentId) if (typeof body.name === "string") updates.name = body.name if (typeof body.type === "string") { const nextType = body.type.trim().toLowerCase() if (nextType !== "url" && nextType !== "file") { return c.json({ error: "type must be 'url' or 'file'" }, 400) } updates.type = nextType } if (body.url === null) { updates.url = null } else if (typeof body.url === "string") { updates.url = body.url } if (body.content === null) { updates.content = null } else if (typeof body.content === "string") { updates.content = body.content } if (typeof body.refreshing === "boolean") { updates.refreshing = body.refreshing } if (body.parentId === null) { updates.parent_id = null } else if (parentId) { updates.parent_id = parentId } if (Object.keys(updates).length <= 1) { return c.json({ error: "No updates provided" }, 400) } try { const database = getDb(c.env) const [item] = await database .update(context_items) .set(updates) .where(eq(context_items.id, itemId)) .returning() if (!item) { return c.json({ error: "Context item not found" }, 404) } return c.json({ item }) } catch (error) { console.error("[worker] update context item failed", error) return c.json({ error: "Failed to update context item" }, 500) } }) app.post("/api/v1/admin/context-items/:itemId/link", async (c) => { const itemId = parseInteger(c.req.param("itemId")) if (!itemId) { return c.json({ error: "itemId required" }, 400) } const body = await parseBody(c) const threadId = parseInteger(body.threadId) if (!threadId) { return c.json({ error: "threadId required" }, 400) } try { const database = getDb(c.env) await database.insert(thread_context_items).values({ thread_id: threadId, context_item_id: itemId, }) return c.json({ success: true }) } catch (error) { console.error("[worker] link context item failed", error) return c.json({ error: "Failed to link context item" }, 500) } }) app.delete("/api/v1/admin/context-items/:itemId", async (c) => { const itemId = parseInteger(c.req.param("itemId")) if (!itemId) { return c.json({ error: "itemId required" }, 400) } try { const database = getDb(c.env) const [item] = await database .delete(context_items) .where(eq(context_items.id, itemId)) .returning() if (!item) { return c.json({ error: "Context item not found" }, 404) } return c.json({ id: itemId }) } catch (error) { console.error("[worker] delete context item failed", error) return c.json({ error: "Failed to delete context item" }, 500) } }) // Browser session endpoints app.post("/api/v1/admin/browser-sessions", async (c) => { const body = await parseBody(c) const userId = typeof body.userId === "string" ? body.userId.trim() : "" const name = typeof body.name === "string" ? body.name.trim() : "" const browser = typeof body.browser === "string" ? body.browser.trim() : "safari" const capturedAt = parseDate(body.capturedAt) const isFavorite = typeof body.isFavorite === "boolean" ? body.isFavorite : undefined const tabs = Array.isArray(body.tabs) ? body.tabs : [] if (!userId || !name) { return c.json({ error: "userId and name are required" }, 400) } const tabValues = tabs .map((tab) => { if (!tab || typeof tab !== "object") { return null } const title = typeof (tab as { title?: unknown }).title === "string" ? (tab as { title: string }).title : "" const url = typeof (tab as { url?: unknown }).url === "string" ? (tab as { url: string }).url : "" if (!url) { return null } const faviconUrl = typeof (tab as { faviconUrl?: unknown }).faviconUrl === "string" ? (tab as { faviconUrl: string }).faviconUrl : typeof (tab as { favicon_url?: unknown }).favicon_url === "string" ? (tab as { favicon_url: string }).favicon_url : null return { title, url, favicon_url: faviconUrl } }) .filter((tab): tab is { title: string; url: string; favicon_url: string | null } => Boolean(tab), ) try { const database = getDb(c.env) const [session] = await database .insert(browser_sessions) .values({ user_id: userId, name, browser, tab_count: tabValues.length, is_favorite: isFavorite ?? false, captured_at: capturedAt ?? new Date(), }) .returning() if (tabValues.length > 0) { await database.insert(browser_session_tabs).values( tabValues.map((tab, index) => ({ session_id: session.id, title: tab.title, url: tab.url, position: index, favicon_url: tab.favicon_url ?? null, })), ) } return c.json({ session }, 201) } catch (error) { console.error("[worker] create browser session failed", error) return c.json({ error: "Failed to create browser session" }, 500) } }) app.patch("/api/v1/admin/browser-sessions/:sessionId", async (c) => { const sessionId = c.req.param("sessionId") if (!sessionId) { return c.json({ error: "sessionId required" }, 400) } const body = await parseBody(c) const updates: Partial = {} if (typeof body.name === "string") updates.name = body.name if (typeof body.isFavorite === "boolean") updates.is_favorite = body.isFavorite if (Object.keys(updates).length === 0) { return c.json({ error: "No updates provided" }, 400) } try { const database = getDb(c.env) const [session] = await database .update(browser_sessions) .set(updates) .where(eq(browser_sessions.id, sessionId)) .returning() if (!session) { return c.json({ error: "Session not found" }, 404) } return c.json({ session }) } catch (error) { console.error("[worker] update browser session failed", error) return c.json({ error: "Failed to update browser session" }, 500) } }) app.delete("/api/v1/admin/browser-sessions/:sessionId", async (c) => { const sessionId = c.req.param("sessionId") if (!sessionId) { return c.json({ error: "sessionId required" }, 400) } try { const database = getDb(c.env) const [session] = await database .delete(browser_sessions) .where(eq(browser_sessions.id, sessionId)) .returning() if (!session) { return c.json({ error: "Session not found" }, 404) } return c.json({ id: sessionId }) } catch (error) { console.error("[worker] delete browser session failed", error) return c.json({ error: "Failed to delete browser session" }, 500) } }) // Export the Hono app as default (handles HTTP requests) export default app // Export the RPC worker for RPC calls via service bindings export { WorkerRpc } from "./rpc"