diff options
| author | Frank <[email protected]> | 2025-07-11 05:01:27 +0800 |
|---|---|---|
| committer | Frank <[email protected]> | 2025-07-11 05:01:27 +0800 |
| commit | 1c4fd7f28ff776953c8f3b191dc19243e6c6c8d1 (patch) | |
| tree | 10f64998872a9d459f7a0deb861c129f81ceea0a | |
| parent | 85805d2c38d0c2e4ddbdc749b5404f316b209c90 (diff) | |
| download | opencode-1c4fd7f28ff776953c8f3b191dc19243e6c6c8d1.tar.gz opencode-1c4fd7f28ff776953c8f3b191dc19243e6c6c8d1.zip | |
Api: add endpoint for getting github app token
| -rw-r--r-- | bun.lock | 38 | ||||
| -rw-r--r-- | infra/app.ts | 4 | ||||
| -rw-r--r-- | packages/function/package.json | 4 | ||||
| -rw-r--r-- | packages/function/src/api.ts | 40 | ||||
| -rw-r--r-- | packages/function/sst-env.d.ts | 24 | ||||
| -rw-r--r-- | packages/opencode/sst-env.d.ts | 2 | ||||
| -rw-r--r-- | packages/web/sst-env.d.ts | 2 | ||||
| -rw-r--r-- | sst-env.d.ts | 26 |
8 files changed, 119 insertions, 21 deletions
@@ -11,6 +11,10 @@ "packages/function": { "name": "@opencode/function", "version": "0.0.1", + "dependencies": { + "@octokit/auth-app": "8.0.1", + "jose": "6.0.11", + }, "devDependencies": { "@cloudflare/workers-types": "4.20250522.0", "@types/node": "catalog:", @@ -337,6 +341,28 @@ "@modelcontextprotocol/sdk": ["@modelcontextprotocol/[email protected]", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="], + "@octokit/auth-app": ["@octokit/[email protected]", "", { "dependencies": { "@octokit/auth-oauth-app": "^9.0.1", "@octokit/auth-oauth-user": "^6.0.0", "@octokit/request": "^10.0.2", "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", "toad-cache": "^3.7.0", "universal-github-app-jwt": "^2.2.0", "universal-user-agent": "^7.0.0" } }, "sha512-P2J5pB3pjiGwtJX4WqJVYCtNkcZ+j5T2Wm14aJAEIC3WJOrv12jvBley3G1U/XI8q9o1A7QMG54LiFED2BiFlg=="], + + "@octokit/auth-oauth-app": ["@octokit/[email protected]", "", { "dependencies": { "@octokit/auth-oauth-device": "^8.0.1", "@octokit/auth-oauth-user": "^6.0.0", "@octokit/request": "^10.0.2", "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-TthWzYxuHKLAbmxdFZwFlmwVyvynpyPmjwc+2/cI3cvbT7mHtsAW9b1LvQaNnAuWL+pFnqtxdmrU8QpF633i1g=="], + + "@octokit/auth-oauth-device": ["@octokit/[email protected]", "", { "dependencies": { "@octokit/oauth-methods": "^6.0.0", "@octokit/request": "^10.0.2", "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-TOqId/+am5yk9zor0RGibmlqn4V0h8vzjxlw/wYr3qzkQxl8aBPur384D1EyHtqvfz0syeXji4OUvKkHvxk/Gw=="], + + "@octokit/auth-oauth-user": ["@octokit/[email protected]", "", { "dependencies": { "@octokit/auth-oauth-device": "^8.0.1", "@octokit/oauth-methods": "^6.0.0", "@octokit/request": "^10.0.2", "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-GV9IW134PHsLhtUad21WIeP9mlJ+QNpFd6V9vuPWmaiN25HEJeEQUcS4y5oRuqCm9iWDLtfIs+9K8uczBXKr6A=="], + + "@octokit/endpoint": ["@octokit/[email protected]", "", { "dependencies": { "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ=="], + + "@octokit/oauth-authorization-url": ["@octokit/[email protected]", "", {}, "sha512-7QoLPRh/ssEA/HuHBHdVdSgF8xNLz/Bc5m9fZkArJE5bb6NmVkDm3anKxXPmN1zh6b5WKZPRr3697xKT/yM3qQ=="], + + "@octokit/oauth-methods": ["@octokit/[email protected]", "", { "dependencies": { "@octokit/oauth-authorization-url": "^8.0.0", "@octokit/request": "^10.0.2", "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0" } }, "sha512-Q8nFIagNLIZgM2odAraelMcDssapc+lF+y3OlcIPxyAU+knefO8KmozGqfnma1xegRDP4z5M73ABsamn72bOcA=="], + + "@octokit/openapi-types": ["@octokit/[email protected]", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="], + + "@octokit/request": ["@octokit/[email protected]", "", { "dependencies": { "@octokit/endpoint": "^11.0.0", "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA=="], + + "@octokit/request-error": ["@octokit/[email protected]", "", { "dependencies": { "@octokit/types": "^14.0.0" } }, "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg=="], + + "@octokit/types": ["@octokit/[email protected]", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="], + "@openauthjs/openauth": ["@openauthjs/[email protected]", "", { "dependencies": { "@standard-schema/spec": "1.0.0-beta.3", "aws4fetch": "1.0.20", "jose": "5.9.6" }, "peerDependencies": { "arctic": "^2.2.2", "hono": "^4.0.0" } }, "sha512-RlnjqvHzqcbFVymEwhlUEuac4utA5h4nhSK/i2szZuQmxTIqbGUxZ+nM+avM+VV4Ing+/ZaNLKILoXS3yrkOOw=="], "@opencode/function": ["@opencode/function@workspace:packages/function"], @@ -807,6 +833,8 @@ "extend": ["[email protected]", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + "fast-content-type-parse": ["[email protected]", "", {}, "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg=="], + "fast-deep-equal": ["[email protected]", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-fifo": ["[email protected]", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="], @@ -993,7 +1021,7 @@ "jmespath": ["[email protected]", "", {}, "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw=="], - "jose": ["[email protected]", "", {}, "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA=="], + "jose": ["[email protected]", "", {}, "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg=="], "joycon": ["[email protected]", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], @@ -1527,6 +1555,8 @@ "tinyglobby": ["[email protected]", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], + "toad-cache": ["[email protected]", "", {}, "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw=="], + "toidentifier": ["[email protected]", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], "token-types": ["[email protected]", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA=="], @@ -1597,6 +1627,10 @@ "unist-util-visit-parents": ["[email protected]", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw=="], + "universal-github-app-jwt": ["[email protected]", "", {}, "sha512-dcmbeSrOdTnsjGjUfAlqNDJrhxXizjAz94ija9Qw8YkZ1uu0d+GoZzyH+Jb9tIIqvGsadUfwg+22k5aDqqwzbw=="], + + "universal-user-agent": ["[email protected]", "", {}, "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A=="], + "unpipe": ["[email protected]", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], "unstorage": ["[email protected]", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^4.0.3", "destr": "^2.0.5", "h3": "^1.15.2", "lru-cache": "^10.4.3", "node-fetch-native": "^1.6.6", "ofetch": "^1.4.1", "ufo": "^1.6.1" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6.0.3 || ^7.0.0", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/kv": "^1.0.1", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-WQ37/H5A7LcRPWfYOrDa1Ys02xAbpPJq6q5GkO88FBXVSQzHd7+BjEwfRqyaSWCv9MbsJy058GWjjPjcJ16GGA=="], @@ -1815,6 +1849,8 @@ "sitemap/sax": ["[email protected]", "", {}, "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg=="], + "sst/jose": ["[email protected]", "", {}, "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA=="], + "token-types/ieee754": ["[email protected]", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], "unicode-trie/pako": ["[email protected]", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="], diff --git a/infra/app.ts b/infra/app.ts index caaea0e9d..5c646d97c 100644 --- a/infra/app.ts +++ b/infra/app.ts @@ -4,6 +4,8 @@ export const domain = (() => { return `${$app.stage}.dev.opencode.ai` })() +const GITHUB_APP_ID = new sst.Secret("GITHUB_APP_ID") +const GITHUB_APP_PRIVATE_KEY = new sst.Secret("GITHUB_APP_PRIVATE_KEY") const bucket = new sst.cloudflare.Bucket("Bucket") export const api = new sst.cloudflare.Worker("Api", { @@ -13,7 +15,7 @@ export const api = new sst.cloudflare.Worker("Api", { WEB_DOMAIN: domain, }, url: true, - link: [bucket], + link: [bucket, GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY], transform: { worker: (args) => { args.logpush = true diff --git a/packages/function/package.json b/packages/function/package.json index 81a1edc92..c033fa058 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -8,5 +8,9 @@ "@cloudflare/workers-types": "4.20250522.0", "typescript": "catalog:", "@types/node": "catalog:" + }, + "dependencies": { + "@octokit/auth-app": "8.0.1", + "jose": "6.0.11" } } diff --git a/packages/function/src/api.ts b/packages/function/src/api.ts index be6ef1923..3ba3d71d1 100644 --- a/packages/function/src/api.ts +++ b/packages/function/src/api.ts @@ -1,5 +1,8 @@ import { DurableObject } from "cloudflare:workers" import { randomUUID } from "node:crypto" +import { jwtVerify, createRemoteJWKSet } from "jose" +import { createAppAuth } from "@octokit/auth-app" +import { Resource } from "sst" type Env = { SYNC_SERVER: DurableObjectNamespace<SyncServer> @@ -218,5 +221,42 @@ export default { }, ) } + + 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` + + // get Authorization header + const authHeader = request.headers.get("Authorization") + const token = authHeader?.replace(/^Bearer /, "") + if (!token) return new Response("Error: authorization header is required", { status: 401 }) + + // verify token + const JWKS = createRemoteJWKSet(new URL(JWKS_URL)) + try { + await jwtVerify(token, JWKS, { + issuer: GITHUB_ISSUER, + audience: EXPECTED_AUDIENCE, + }) + } 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" }, + }) + } + + // Create app token + const auth = createAppAuth({ + appId: Resource.GITHUB_APP_ID.value, + privateKey: Resource.GITHUB_APP_PRIVATE_KEY.value, + }) + const appAuthentication = await auth({ type: "app" }) + + return new Response(JSON.stringify({ token: appAuthentication.token }), { + headers: { "Content-Type": "application/json" }, + }) + } }, } diff --git a/packages/function/sst-env.d.ts b/packages/function/sst-env.d.ts index fd95edbb4..dab7de3f3 100644 --- a/packages/function/sst-env.d.ts +++ b/packages/function/sst-env.d.ts @@ -6,20 +6,28 @@ import "sst" declare module "sst" { export interface Resource { - Web: { - type: "sst.cloudflare.Astro" - url: string + "GITHUB_APP_ID": { + "type": "sst.sst.Secret" + "value": string + } + "GITHUB_APP_PRIVATE_KEY": { + "type": "sst.sst.Secret" + "value": string + } + "Web": { + "type": "sst.cloudflare.Astro" + "url": string } } } -// cloudflare -import * as cloudflare from "@cloudflare/workers-types" +// cloudflare +import * as cloudflare from "@cloudflare/workers-types"; declare module "sst" { export interface Resource { - Api: cloudflare.Service - Bucket: cloudflare.R2Bucket + "Api": cloudflare.Service + "Bucket": cloudflare.R2Bucket } } import "sst" -export {} +export {}
\ No newline at end of file diff --git a/packages/opencode/sst-env.d.ts b/packages/opencode/sst-env.d.ts index 0397645b5..b6a7e9066 100644 --- a/packages/opencode/sst-env.d.ts +++ b/packages/opencode/sst-env.d.ts @@ -6,4 +6,4 @@ /// <reference path="../../sst-env.d.ts" /> import "sst" -export {} +export {}
\ No newline at end of file diff --git a/packages/web/sst-env.d.ts b/packages/web/sst-env.d.ts index 0397645b5..b6a7e9066 100644 --- a/packages/web/sst-env.d.ts +++ b/packages/web/sst-env.d.ts @@ -6,4 +6,4 @@ /// <reference path="../../sst-env.d.ts" /> import "sst" -export {} +export {}
\ No newline at end of file diff --git a/sst-env.d.ts b/sst-env.d.ts index 45c07b66c..2c3e3d5ad 100644 --- a/sst-env.d.ts +++ b/sst-env.d.ts @@ -5,20 +5,28 @@ declare module "sst" { export interface Resource { - Api: { - type: "sst.cloudflare.Worker" - url: string + "Api": { + "type": "sst.cloudflare.Worker" + "url": string } - Bucket: { - type: "sst.cloudflare.Bucket" + "Bucket": { + "type": "sst.cloudflare.Bucket" } - Web: { - type: "sst.cloudflare.Astro" - url: string + "GITHUB_APP_ID": { + "type": "sst.sst.Secret" + "value": string + } + "GITHUB_APP_PRIVATE_KEY": { + "type": "sst.sst.Secret" + "value": string + } + "Web": { + "type": "sst.cloudflare.Astro" + "url": string } } } /// <reference path="sst-env.d.ts" /> import "sst" -export {} +export {}
\ No newline at end of file |
