Compare commits

...
Author SHA1 Message Date
Aiden Cline d1b6cbc6a0 fix(github): support immutable OIDC subjects 2026-08-04 03:29:45 +00:00
4 changed files with 55 additions and 10 deletions
+5 -7
View File
@@ -5,6 +5,7 @@ import { jwtVerify, createRemoteJWKSet } from "jose"
import { createAppAuth } from "@octokit/auth-app"
import { Octokit } from "@octokit/rest"
import { Resource } from "sst"
import { parseRepositoryClaim } from "./github"
type Env = {
SYNC_SERVER: DurableObjectNamespace<SyncServer>
@@ -269,16 +270,13 @@ export default new Hono<{ Bindings: Env }>()
// verify token
const JWKS = createRemoteJWKSet(new URL(JWKS_URL))
let owner, repo
let repository: ReturnType<typeof parseRepositoryClaim>
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]
repository = parseRepositoryClaim(payload)
} catch (err) {
console.error("Token verification failed:", err)
return c.json({ error: "Invalid or expired token" }, { status: 403 })
@@ -294,8 +292,8 @@ export default new Hono<{ Bindings: Env }>()
// Lookup installation
const octokit = new Octokit({ auth: appAuth.token })
const { data: installation } = await octokit.apps.getRepoInstallation({
owner,
repo,
owner: repository.owner,
repo: repository.repo,
})
// Get installation token
+14
View File
@@ -0,0 +1,14 @@
import type { JWTPayload } from "jose"
export function parseRepositoryClaim(payload: JWTPayload) {
const claim = payload.repository
if (typeof claim !== "string") throw new Error("Repository claim is missing")
const parts = claim.split("/")
if (parts.length !== 2 || !parts[0] || !parts[1]) throw new Error("Repository claim is invalid")
return {
owner: parts[0],
repo: parts[1],
}
}
+30
View File
@@ -0,0 +1,30 @@
import { describe, expect, test } from "bun:test"
import { parseRepositoryClaim } from "../src/github"
describe("parseRepositoryClaim", () => {
test("reads repository identity independently of the legacy subject format", () => {
expect(
parseRepositoryClaim({
repository: "octocat/my-repo",
sub: "repo:octocat/my-repo:ref:refs/heads/main",
}),
).toEqual({ owner: "octocat", repo: "my-repo" })
})
test("reads repository identity with an immutable subject format", () => {
expect(
parseRepositoryClaim({
repository: "octocat/my-repo",
sub: "repo:octocat@123456/my-repo@456789:ref:refs/heads/main",
}),
).toEqual({ owner: "octocat", repo: "my-repo" })
})
test("rejects a missing repository claim", () => {
expect(() => parseRepositoryClaim({})).toThrow("Repository claim is missing")
})
test("rejects an invalid repository claim", () => {
expect(() => parseRepositoryClaim({ repository: "octocat" })).toThrow("Repository claim is invalid")
})
})
@@ -437,6 +437,7 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?:
let session: { id: SessionID; title: string; version: string }
let shareId: string | undefined
let exitCode = 0
let canComment = false
type PromptFiles = Awaited<ReturnType<typeof getUserPrompt>>["promptFiles"]
const triggerCommentId = isCommentEvent
? (payload as IssueCommentEvent | PullRequestReviewCommentEvent).comment.id
@@ -485,6 +486,7 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?:
octoGraph = graphql.defaults({
headers: { authorization: `token ${appToken}` },
})
canComment = true
const { userPrompt, promptFiles } = await getUserPrompt()
if (!useGithubToken) {
@@ -639,7 +641,7 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?:
} else if (e instanceof Error) {
msg = e.message
}
if (isUserEvent) {
if (isUserEvent && canComment) {
await createComment(`${msg}${footer()}`)
await removeReaction(commentType)
}
@@ -1004,8 +1006,9 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?:
})
if (!response.ok) {
const responseJson = (await response.json()) as { error?: string }
throw new Error(`App token exchange failed: ${response.status} ${response.statusText} - ${responseJson.error}`)
throw new Error(
`App token exchange failed: ${response.status} ${response.statusText} - ${await response.text()}`,
)
}
const responseJson = (await response.json()) as { token: string }