import { Hono } from "hono" import { DurableObject } from "cloudflare:workers" import { randomUUID } from "node:crypto" import { jwtVerify, createRemoteJWKSet } from "jose" import { createAppAuth } from "@octokit/auth-app" import { Octokit } from "@octokit/rest" import { Resource } from "sst" type Env = { SYNC_SERVER: DurableObjectNamespace Bucket: R2Bucket WEB_DOMAIN: string } export class SyncServer extends DurableObject { constructor(ctx: DurableObjectState, env: Env) { super(ctx, env) } async fetch() { console.log("SyncServer subscribe") const webSocketPair = new WebSocketPair() const [client, server] = Object.values(webSocketPair) this.ctx.acceptWebSocket(server) const data = await this.ctx.storage.list() Array.from(data.entries()) .filter(([key, _]) => key.startsWith("session/")) .map(([key, content]) => server.send(JSON.stringify({ key, content }))) return new Response(null, { status: 101, webSocket: client, }) } async webSocketMessage(ws, message) {} async webSocketClose(ws, code, reason, wasClean) { ws.close(code, "Durable Object is closing WebSocket") } async publish(key: string, content: any) { const sessionID = await this.getSessionID() if ( !key.startsWith(`session/info/${sessionID}`) && !key.startsWith(`session/message/${sessionID}/`) && !key.startsWith(`session/part/${sessionID}/`) ) return new Response("Error: Invalid key", { status: 400 }) // store message await this.env.Bucket.put(`share/${key}.json`, JSON.stringify(content), { httpMetadata: { contentType: "application/json", }, }) await this.ctx.storage.put(key, content) const clients = this.ctx.getWebSockets() console.log("SyncServer publish", key, "to", clients.length, "subscribers") for (const client of clients) { client.send(JSON.stringify({ key, content })) } } public async share(sessionID: string) { let secret = await this.getSecret() if (secret) return secret secret = randomUUID() await this.ctx.storage.put("secret", secret) await this.ctx.storage.put("sessionID", sessionID) return secret } public async getData() { const data = (await this.ctx.storage.list()) as Map return Array.from(data.entries()) .filter(([key, _]) => key.startsWith("session/")) .map(([key, content]) => ({ key, content })) } public async assertSecret(secret: string) { if (secret !== (await this.getSecret())) throw new Error("Invalid secret") } private async getSecret() { return this.ctx.storage.get("secret") } private async getSessionID() { return this.ctx.storage.get("sessionID") } async clear() { const sessionID = await this.getSessionID() const list = await this.env.Bucket.list({ prefix: `session/message/${sessionID}/`, limit: 1000, }) for (const item of list.objects) { await this.env.Bucket.delete(item.key) } await this.env.Bucket.delete(`session/info/${sessionID}`) await this.ctx.storage.deleteAll() } static shortName(id: string) { return id.substring(id.length - 8) } } export default new Hono<{ Bindings: Env }>() .get("/", (c) => c.text("Hello, world!")) .post("/share_create", async (c) => { const body = await c.req.json<{ sessionID: string }>() const sessionID = body.sessionID const short = SyncServer.shortName(sessionID) const id = c.env.SYNC_SERVER.idFromName(short) const stub = c.env.SYNC_SERVER.get(id) const secret = await stub.share(sessionID) return c.json({ secret, url: `https://${c.env.WEB_DOMAIN}/s/${short}`, }) }) .post("/share_delete", async (c) => { const body = await c.req.json<{ sessionID: string; secret: string }>() const sessionID = body.sessionID const secret = body.secret const id = c.env.SYNC_SERVER.idFromName(SyncServer.shortName(sessionID)) const stub = c.env.SYNC_SERVER.get(id) await stub.assertSecret(secret) await stub.clear() return c.json({}) }) .post("/share_delete_admin", async (c) => { const id = c.env.SYNC_SERVER.idFromName("oVF8Rsiv") const stub = c.env.SYNC_SERVER.get(id) await stub.clear() return c.json({}) }) .post("/share_sync", async (c) => { const body = await c.req.json<{ sessionID: string secret: string key: string content: any }>() const name = SyncServer.shortName(body.sessionID) const id = c.env.SYNC_SERVER.idFromName(name) const stub = c.env.SYNC_SERVER.get(id) await stub.assertSecret(body.secret) await stub.publish(body.key, body.content) return c.json({}) }) .get("/share_poll", async (c) => { const upgradeHeader = c.req.header("Upgrade") if (!upgradeHeader || upgradeHeader !== "websocket") { return c.text("Error: Upgrade header is required", { status: 426 }) } const id = c.req.query("id") console.log("share_poll", id) if (!id) return c.text("Error: Share ID is required", { status: 400 }) const stub = c.env.SYNC_SERVER.get(c.env.SYNC_SERVER.idFromName(id)) return stub.fetch(c.req.raw) }) .get("/share_data", async (c) => { const id = c.req.query("id") console.log("share_data", id) if (!id) return c.text("Error: Share ID is required", { status: 400 }) const stub = c.env.SYNC_SERVER.get(c.env.SYNC_SERVER.idFromName(id)) const data = await stub.getData() let info const messages: Record = {} data.forEach((d) => { const [root, type, ...splits] = d.key.split("/") if (root !== "session") return if (type === "info") { info = d.content return } if (type === "message") { messages[d.content.id] = { parts: [], ...d.content, } } if (type === "part") { messages[d.content.messageID].parts.push(d.content) } }) return c.json({ info, messages }) }) /** * Used by the GitHub action to get GitHub installation access token given the OIDC token */ .post("/exchange_github_app_token", async (c) => { const EXPECTED_AUDIENCE = "opencode-github-action" const GITHUB_ISSUER = "https://token.actions.githubusercontent.com" const JWKS_URL = `${GITHUB_ISSUER}/.well-known/jwks` // get Authorization header const token = c.req.header("Authorization")?.replace(/^Bearer /, "") if (!token) return c.json({ error: "Authorization header is required" }, { status: 401 }) // verify token const JWKS = createRemoteJWKSet(new URL(JWKS_URL)) let owner, repo try { const { payload } = await jwtVerify(token, JWKS, { issuer: GITHUB_ISSUER, audience: EXPECTED_AUDIENCE, }) const sub = payload.sub // e.g. 'repo:my-org/my-repo:ref:refs/heads/main' const parts = sub.split(":")[1].split("/") owner = parts[0] repo = parts[1] } catch (err) { console.error("Token verification failed:", err) return c.json({ error: "Invalid or expired token" }, { status: 403 }) } // Create app JWT token const auth = createAppAuth({ appId: Resource.GITHUB_APP_ID.value, privateKey: Resource.GITHUB_APP_PRIVATE_KEY.value, }) const appAuth = await auth({ type: "app" }) // Lookup installation const octokit = new Octokit({ auth: appAuth.token }) const { data: installation } = await octokit.apps.getRepoInstallation({ owner, repo }) // Get installation token const installationAuth = await auth({ type: "installation", installationId: installation.id }) return c.json({ token: installationAuth.token }) }) /** * Used by the GitHub action to get GitHub installation access token given user PAT token (used when testing `opencode github run` locally) */ .post("/exchange_github_app_token_with_pat", async (c) => { const body = await c.req.json<{ owner: string; repo: string }>() const owner = body.owner const repo = body.repo try { // get Authorization header const authHeader = c.req.header("Authorization") const token = authHeader?.replace(/^Bearer /, "") if (!token) throw new Error("Authorization header is required") // Verify permissions const userClient = new Octokit({ auth: token }) const { data: repoData } = await userClient.repos.get({ owner, repo }) if (!repoData.permissions.admin && !repoData.permissions.push && !repoData.permissions.maintain) throw new Error("User does not have write permissions") // Get installation token const auth = createAppAuth({ appId: Resource.GITHUB_APP_ID.value, privateKey: Resource.GITHUB_APP_PRIVATE_KEY.value, }) const appAuth = await auth({ type: "app" }) // Lookup installation const appClient = new Octokit({ auth: appAuth.token }) const { data: installation } = await appClient.apps.getRepoInstallation({ owner, repo }) // Get installation token const installationAuth = await auth({ type: "installation", installationId: installation.id }) return c.json({ token: installationAuth.token }) } catch (e: any) { let error = e if (e instanceof Error) { error = e.message } return c.json({ error }, { status: 401 }) } }) /** * Used by the opencode CLI to check if the GitHub app is installed */ .get("/get_github_app_installation", async (c) => { const owner = c.req.query("owner") const repo = c.req.query("repo") const auth = createAppAuth({ appId: Resource.GITHUB_APP_ID.value, privateKey: Resource.GITHUB_APP_PRIVATE_KEY.value, }) const appAuth = await auth({ type: "app" }) // Lookup installation const octokit = new Octokit({ auth: appAuth.token }) let installation try { const ret = await octokit.apps.getRepoInstallation({ owner, repo }) installation = ret.data } catch (err) { if (err instanceof Error && err.message.includes("Not Found")) { // not installed } else { throw err } } return c.json({ installation }) }) .all("*", (c) => c.text("Not Found"))