Просмотр исходного кода

convert share backend to hono app

Frank 6 месяцев назад
Родитель
Сommit
4a46144419

+ 3 - 1
bun.lock

@@ -14,6 +14,7 @@
       "dependencies": {
         "@octokit/auth-app": "8.0.1",
         "@octokit/rest": "22.0.0",
+        "hono": "catalog:",
         "jose": "6.0.11",
       },
       "devDependencies": {
@@ -41,7 +42,7 @@
         "decimal.js": "10.5.0",
         "diff": "8.0.2",
         "gray-matter": "4.0.3",
-        "hono": "4.7.10",
+        "hono": "catalog:",
         "hono-openapi": "0.4.8",
         "isomorphic-git": "1.32.1",
         "open": "10.1.2",
@@ -133,6 +134,7 @@
   "catalog": {
     "@types/node": "22.13.9",
     "ai": "5.0.0-beta.21",
+    "hono": "4.7.10",
     "typescript": "5.8.2",
     "zod": "3.25.49",
   },

+ 4 - 3
package.json

@@ -15,10 +15,11 @@
       "packages/*"
     ],
     "catalog": {
-      "typescript": "5.8.2",
       "@types/node": "22.13.9",
-      "zod": "3.25.49",
-      "ai": "5.0.0-beta.21"
+      "ai": "5.0.0-beta.21",
+      "hono": "4.7.10",
+      "typescript": "5.8.2",
+      "zod": "3.25.49"
     }
   },
   "devDependencies": {

+ 1 - 0
packages/function/package.json

@@ -12,6 +12,7 @@
   "dependencies": {
     "@octokit/auth-app": "8.0.1",
     "@octokit/rest": "22.0.0",
+    "hono": "catalog:",
     "jose": "6.0.11"
   }
 }

+ 179 - 237
packages/function/src/api.ts

@@ -1,3 +1,4 @@
+import { Hono } from "hono"
 import { DurableObject } from "cloudflare:workers"
 import { randomUUID } from "node:crypto"
 import { jwtVerify, createRemoteJWKSet } from "jose"
@@ -111,165 +112,156 @@ export class SyncServer extends DurableObject<Env> {
   }
 }
 
-export default {
-  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
-    const url = new URL(request.url)
-    const splits = url.pathname.split("/")
-    const method = splits[1]
-
-    if (request.method === "GET" && method === "") {
-      return new Response("Hello, world!", {
-        headers: { "Content-Type": "text/plain" },
-      })
-    }
-
-    if (request.method === "POST" && method === "share_create") {
-      const body = await request.json<any>()
-      const sessionID = body.sessionID
-      const short = SyncServer.shortName(sessionID)
-      const id = env.SYNC_SERVER.idFromName(short)
-      const stub = env.SYNC_SERVER.get(id)
-      const secret = await stub.share(sessionID)
-      return new Response(
-        JSON.stringify({
-          secret,
-          url: `https://${env.WEB_DOMAIN}/s/${short}`,
-        }),
-        {
-          headers: { "Content-Type": "application/json" },
-        },
-      )
-    }
-
-    if (request.method === "POST" && method === "share_delete") {
-      const body = await request.json<any>()
-      const sessionID = body.sessionID
-      const secret = body.secret
-      const id = env.SYNC_SERVER.idFromName(SyncServer.shortName(sessionID))
-      const stub = env.SYNC_SERVER.get(id)
-      await stub.assertSecret(secret)
-      await stub.clear()
-      return new Response(JSON.stringify({}), {
-        headers: { "Content-Type": "application/json" },
-      })
-    }
-
-    if (request.method === "POST" && method === "share_delete_admin") {
-      const id = env.SYNC_SERVER.idFromName("oVF8Rsiv")
-      const stub = env.SYNC_SERVER.get(id)
-      await stub.clear()
-      return new Response(JSON.stringify({}), {
-        headers: { "Content-Type": "application/json" },
-      })
-    }
-
-    if (request.method === "POST" && method === "share_sync") {
-      const body = await request.json<{
-        sessionID: string
-        secret: string
-        key: string
-        content: any
-      }>()
-      const name = SyncServer.shortName(body.sessionID)
-      const id = env.SYNC_SERVER.idFromName(name)
-      const stub = env.SYNC_SERVER.get(id)
-      await stub.assertSecret(body.secret)
-      await stub.publish(body.key, body.content)
-      return new Response(JSON.stringify({}), {
-        headers: { "Content-Type": "application/json" },
-      })
+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 })
     }
-
-    if (request.method === "GET" && method === "share_poll") {
-      const upgradeHeader = request.headers.get("Upgrade")
-      if (!upgradeHeader || upgradeHeader !== "websocket") {
-        return new Response("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<string, any> = {}
+    data.forEach((d) => {
+      const [root, type, ...splits] = d.key.split("/")
+      if (root !== "session") return
+      if (type === "info") {
+        info = d.content
+        return
       }
-      const id = url.searchParams.get("id")
-      console.log("share_poll", id)
-      if (!id) return new Response("Error: Share ID is required", { status: 400 })
-      const stub = env.SYNC_SERVER.get(env.SYNC_SERVER.idFromName(id))
-      return stub.fetch(request)
-    }
-
-    if (request.method === "GET" && method === "share_data") {
-      const id = url.searchParams.get("id")
-      console.log("share_data", id)
-      if (!id) return new Response("Error: Share ID is required", { status: 400 })
-      const stub = env.SYNC_SERVER.get(env.SYNC_SERVER.idFromName(id))
-      const data = await stub.getData()
-
-      let info
-      const messages: Record<string, any> = {}
-      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)
+      if (type === "message") {
+        messages[d.content.id] = {
+          parts: [],
+          ...d.content,
         }
-      })
+      }
+      if (type === "part") {
+        messages[d.content.messageID].parts.push(d.content)
+      }
+    })
 
-      return new Response(
-        JSON.stringify({
-          info,
-          messages,
-        }),
-        {
-          headers: { "Content-Type": "application/json" },
-        },
-      )
+    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 })
     }
 
-    /**
-     * Used by the GitHub action to get GitHub installation access token given the OIDC token
-     */
-    if (request.method === "POST" && method === "exchange_github_app_token") {
-      const EXPECTED_AUDIENCE = "opencode-github-action"
-      const GITHUB_ISSUER = "https://token.actions.githubusercontent.com"
-      const JWKS_URL = `${GITHUB_ISSUER}/.well-known/jwks`
-
+    // 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 = request.headers.get("Authorization")
+      const authHeader = c.req.header("Authorization")
       const token = authHeader?.replace(/^Bearer /, "")
-      if (!token)
-        return new Response(JSON.stringify({ error: "Authorization header is required" }), {
-          status: 401,
-          headers: { "Content-Type": "application/json" },
-        })
-
-      // 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 new Response(JSON.stringify({ error: "Invalid or expired token" }), {
-          status: 403,
-          headers: { "Content-Type": "application/json" },
-        })
-      }
+      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")
 
-      // Create app JWT token
+      // Get installation token
       const auth = createAppAuth({
         appId: Resource.GITHUB_APP_ID.value,
         privateKey: Resource.GITHUB_APP_PRIVATE_KEY.value,
@@ -277,99 +269,49 @@ export default {
       const appAuth = await auth({ type: "app" })
 
       // Lookup installation
-      const octokit = new Octokit({ auth: appAuth.token })
-      const { data: installation } = await octokit.apps.getRepoInstallation({ owner, repo })
+      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 new Response(JSON.stringify({ token: installationAuth.token }), {
-        headers: { "Content-Type": "application/json" },
-      })
-    }
-
-    /**
-     * Used by the GitHub action to get GitHub installation access token given user PAT token (used when testing `opencode github run` locally)
-     */
-    if (request.method === "POST" && method === "exchange_github_app_token_with_pat") {
-      const body = await request.json<any>()
-      const owner = body.owner
-      const repo = body.repo
-
-      try {
-        // get Authorization header
-        const authHeader = request.headers.get("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 new Response(JSON.stringify({ token: installationAuth.token }), {
-          headers: { "Content-Type": "application/json" },
-        })
-      } catch (e: any) {
-        let error = e
-        if (e instanceof Error) {
-          error = e.message
-        }
-
-        return new Response(JSON.stringify({ error }), {
-          status: 401,
-          headers: { "Content-Type": "application/json" },
-        })
+      return c.json({ token: installationAuth.token })
+    } catch (e: any) {
+      let error = e
+      if (e instanceof Error) {
+        error = e.message
       }
-    }
-
-    /**
-     * Used by the opencode CLI to check if the GitHub app is installed
-     */
-    if (request.method === "GET" && method === "get_github_app_installation") {
-      const owner = url.searchParams.get("owner")
-      const repo = url.searchParams.get("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({ 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 new Response(JSON.stringify({ installation }), {
-        headers: { "Content-Type": "application/json" },
-      })
     }
 
-    return new Response("Not Found", { status: 404 })
-  },
-}
+    return c.json({ installation })
+  })
+  .all("*", (c) => c.text("Not Found"))

+ 1 - 1
packages/opencode/package.json

@@ -40,7 +40,7 @@
     "decimal.js": "10.5.0",
     "diff": "8.0.2",
     "gray-matter": "4.0.3",
-    "hono": "4.7.10",
+    "hono": "catalog:",
     "hono-openapi": "0.4.8",
     "isomorphic-git": "1.32.1",
     "open": "10.1.2",

+ 1 - 1
packages/opencode/src/cli/cmd/github.ts

@@ -274,7 +274,7 @@ export const GithubInstallCommand = cmd({
 
         // Wait for installation
         s.message("Waiting for GitHub app to be installed")
-        const MAX_RETRIES = 60
+        const MAX_RETRIES = 120
         let retries = 0
         do {
           const installation = await getInstallation()

+ 9 - 0
packages/sdk/sst-env.d.ts

@@ -0,0 +1,9 @@
+/* This file is auto-generated by SST. Do not edit. */
+/* tslint:disable */
+/* eslint-disable */
+/* deno-fmt-ignore-file */
+
+/// <reference path="../../sst-env.d.ts" />
+
+import "sst"
+export {}

+ 9 - 0
sdks/vscode/sst-env.d.ts

@@ -0,0 +1,9 @@
+/* This file is auto-generated by SST. Do not edit. */
+/* tslint:disable */
+/* eslint-disable */
+/* deno-fmt-ignore-file */
+
+/// <reference path="../../sst-env.d.ts" />
+
+import "sst"
+export {}