From a6b036bcde72fed5cd3f524dff56780e3d1f9621 Mon Sep 17 00:00:00 2001 From: ghost <49853598+JSONbored@users.noreply.github.com> Date: Sun, 28 Jun 2026 03:41:57 -0700 Subject: [PATCH] fix(orb): cache brokered installation tokens --- src/orb/broker.ts | 56 +++++++++++++++++- test/integration/orb-broker.test.ts | 88 ++++++++++++++++++++++++++++- 2 files changed, 140 insertions(+), 4 deletions(-) diff --git a/src/orb/broker.ts b/src/orb/broker.ts index 3891861b47..a3af7cd129 100644 --- a/src/orb/broker.ts +++ b/src/orb/broker.ts @@ -12,8 +12,13 @@ // the installation's account server-side before issuing — both bind installation_id at issue time, so the OAuth // privilege-escalation surface the red-team flagged stays closed. import { createOpaqueToken, hashToken } from "../auth/security"; +import { decryptSecret, encryptSecret } from "../utils/crypto"; import { createOrbInstallationToken } from "./app-auth"; +// A minted GitHub installation token lasts ~1h; re-mint only once it is under this margin so a near-expiry +// entry is never handed out (covers clock skew + the engine's own ~5m cache margin). +const ORB_TOKEN_CACHE_MIN_REMAINING_MS = 10 * 60_000; + export function isOrbBrokerEnabled(env: Env): boolean { return /^(1|true|yes|on)$/i.test(String(env.ORB_BROKER_ENABLED ?? "").trim()); } @@ -50,16 +55,61 @@ export type BrokerResult = { token: string; installationId: number; expiresAt: s * registered=1 and neither suspended nor removed at mint time (the gate is re-checked, not trusted from issue). */ export async function brokerOrbToken(env: Env, secret: string): Promise { const row = await env.DB - .prepare("SELECT enroll_id, installation_id, state, revoked_at FROM orb_enrollments WHERE secret_hash = ?") + .prepare("SELECT enroll_id, installation_id, state, revoked_at, cached_token_json FROM orb_enrollments WHERE secret_hash = ?") .bind(await hashToken(secret)) - .first<{ enroll_id: string; installation_id: number; state: string; revoked_at: string | null }>(); + .first<{ enroll_id: string; installation_id: number; state: string; revoked_at: string | null; cached_token_json: string | null }>(); if (!row || row.state !== "enrolled" || row.revoked_at !== null) return { error: "invalid_enrollment" }; const install = await env.DB .prepare("SELECT registered, suspended_at, removed_at FROM orb_github_installations WHERE installation_id = ?") .bind(row.installation_id) .first<{ registered: number; suspended_at: string | null; removed_at: string | null }>(); if (!install || install.registered !== 1 || install.suspended_at !== null || install.removed_at !== null) return { error: "installation_not_eligible" }; + // Serve a still-fresh cached token instead of re-minting. GitHub installation tokens last ~1h, and minting on + // every broker call can throttle GitHub's token endpoint (slow responses -> engine timeouts -> unavailable orb). + // The token is cached encrypted-at-rest (AES-256-GCM via TOKEN_ENCRYPTION_SECRET); with no key set the cache is + // skipped and we mint every call exactly as before. + const cached = await readCachedOrbToken(env, row.cached_token_json); + if (cached) { + await touchLastToken(env, row.enroll_id); + return { token: cached.token, installationId: row.installation_id, expiresAt: cached.expiresAt }; + } const minted = await createOrbInstallationToken(env, row.installation_id); - await env.DB.prepare("UPDATE orb_enrollments SET last_token_at = CURRENT_TIMESTAMP WHERE enroll_id = ?").bind(row.enroll_id).run(); + await cacheOrbToken(env, row.enroll_id, minted); + await touchLastToken(env, row.enroll_id); return { token: minted.token, installationId: row.installation_id, expiresAt: minted.expiresAt }; } + +async function touchLastToken(env: Env, enrollId: string): Promise { + try { + await env.DB.prepare("UPDATE orb_enrollments SET last_token_at = CURRENT_TIMESTAMP WHERE enroll_id = ?").bind(enrollId).run(); + } catch (error) { + console.warn(JSON.stringify({ level: "warn", event: "orb_token_last_touch_failed", enrollId, message: String(error).slice(0, 120) })); + } +} + +/** Decrypt + return the cached installation token when present and still safely before expiry; null (→ re-mint) on + * no key, no cache, an expired/unparseable entry, or any decrypt failure (e.g. a rotated encryption key). */ +async function readCachedOrbToken(env: Env, cachedJson: string | null): Promise<{ token: string; expiresAt: string } | null> { + if (!env.TOKEN_ENCRYPTION_SECRET || !cachedJson) return null; + try { + const entry = JSON.parse(cachedJson) as { ciphertext: string; iv: string; salt: string | null; expiresAt: string }; + if (!(Date.parse(entry.expiresAt) - Date.now() >= ORB_TOKEN_CACHE_MIN_REMAINING_MS)) return null; + const token = await decryptSecret(entry.ciphertext, entry.iv, env.TOKEN_ENCRYPTION_SECRET, entry.salt); + return { token, expiresAt: entry.expiresAt }; + } catch { + return null; + } +} + +/** Cache the freshly minted token (encrypted) on the enrollment row. Best-effort + fail-safe: a cache-write error + * must never fail a valid token exchange — the next call simply re-mints. No-op without an encryption key. */ +async function cacheOrbToken(env: Env, enrollId: string, minted: { token: string; expiresAt: string }): Promise { + if (!env.TOKEN_ENCRYPTION_SECRET) return; + try { + const enc = await encryptSecret(minted.token, env.TOKEN_ENCRYPTION_SECRET); + const json = JSON.stringify({ ciphertext: enc.ciphertext, iv: enc.iv, salt: enc.salt, expiresAt: minted.expiresAt }); + await env.DB.prepare("UPDATE orb_enrollments SET cached_token_json = ? WHERE enroll_id = ?").bind(json, enrollId).run(); + } catch (error) { + console.warn(JSON.stringify({ level: "warn", event: "orb_token_cache_write_failed", enrollId, message: String(error).slice(0, 120) })); + } +} diff --git a/test/integration/orb-broker.test.ts b/test/integration/orb-broker.test.ts index 33128be800..531089a46e 100644 --- a/test/integration/orb-broker.test.ts +++ b/test/integration/orb-broker.test.ts @@ -17,8 +17,19 @@ const seedInstall = (e: Env, id: number, cols: Record = {}): Promise => createTestEnv({ ORB_BROKER_ENABLED: "true", ORB_GITHUB_APP_ID: "4139483", ORB_GITHUB_APP_PRIVATE_KEY: await pkcs8Pem(), INTERNAL_JOB_TOKEN: "dev-internal-token", ...over }); const tokenFetch = (token = "ghs_broker", expires = "2026-06-25T08:00:00Z") => vi.stubGlobal("fetch", async () => Response.json({ token, expires_at: expires })); +const countingTokenFetch = (expires = "2026-06-25T08:00:00Z") => { + let calls = 0; + vi.stubGlobal("fetch", async () => { + calls += 1; + return Response.json({ token: `ghs_minted_${calls}`, expires_at: expires }); + }); + return () => calls; +}; -afterEach(() => vi.unstubAllGlobals()); +afterEach(() => { + vi.useRealTimers(); + vi.unstubAllGlobals(); +}); describe("isOrbBrokerEnabled", () => { it("is off by default, on for a truthy flag", () => { @@ -52,6 +63,81 @@ describe("brokerOrbToken", () => { expect((await db(e).prepare("SELECT last_token_at FROM orb_enrollments WHERE installation_id=300").first<{ last_token_at: string | null }>())?.last_token_at).not.toBeNull(); }); + it("caches a freshly minted token and serves repeated exchanges without reminting", async () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-06-25T07:00:00Z")); + const e = await brokerEnv({ TOKEN_ENCRYPTION_SECRET: "orb-cache-test-secret" }); + await seedInstall(e, 303, { registered: 1 }); + const { secret } = (await issueOrbEnrollment(e, 303)) as { secret: string }; + const fetchCalls = countingTokenFetch("2026-06-25T08:00:00Z"); + + expect(await brokerOrbToken(e, secret)).toEqual({ token: "ghs_minted_1", installationId: 303, expiresAt: "2026-06-25T08:00:00Z" }); + expect(await brokerOrbToken(e, secret)).toEqual({ token: "ghs_minted_1", installationId: 303, expiresAt: "2026-06-25T08:00:00Z" }); + expect(fetchCalls()).toBe(1); + const row = await db(e).prepare("SELECT cached_token_json FROM orb_enrollments WHERE installation_id=303").first<{ cached_token_json: string }>(); + expect(row?.cached_token_json).toContain("ciphertext"); + expect(row?.cached_token_json).not.toContain("ghs_minted_1"); + }); + + it("remints when the encrypted cache is absent, expired, or unreadable", async () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-06-25T07:00:00Z")); + const e = await brokerEnv({ TOKEN_ENCRYPTION_SECRET: "orb-cache-test-secret" }); + await seedInstall(e, 304, { registered: 1 }); + const { secret } = (await issueOrbEnrollment(e, 304)) as { secret: string }; + const fetchCalls = countingTokenFetch("2026-06-25T08:00:00Z"); + + expect(await brokerOrbToken(e, secret)).toMatchObject({ token: "ghs_minted_1" }); + await db(e).prepare("UPDATE orb_enrollments SET cached_token_json = ? WHERE installation_id = 304").bind(JSON.stringify({ ciphertext: "bad", iv: "bad", salt: null, expiresAt: "2026-06-25T08:00:00Z" })).run(); + expect(await brokerOrbToken(e, secret)).toMatchObject({ token: "ghs_minted_2" }); + await db(e).prepare("UPDATE orb_enrollments SET cached_token_json = ? WHERE installation_id = 304").bind(JSON.stringify({ ciphertext: "bad", iv: "bad", salt: null, expiresAt: "2026-06-25T07:05:00Z" })).run(); + expect(await brokerOrbToken(e, secret)).toMatchObject({ token: "ghs_minted_3" }); + expect(fetchCalls()).toBe(3); + }); + + it("still returns a minted token when writing the encrypted cache fails", async () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-06-25T07:00:00Z")); + const e = await brokerEnv({ TOKEN_ENCRYPTION_SECRET: "orb-cache-test-secret" }); + await seedInstall(e, 305, { registered: 1 }); + const { secret } = (await issueOrbEnrollment(e, 305)) as { secret: string }; + tokenFetch("ghs_cache_write_failed", "2026-06-25T08:00:00Z"); + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + const originalPrepare = db(e).prepare.bind(db(e)); + vi.spyOn(db(e), "prepare").mockImplementation((sql: string) => { + if (sql.includes("SET cached_token_json = ?")) { + throw new Error("cache write unavailable"); + } + return originalPrepare(sql); + }); + + expect(await brokerOrbToken(e, secret)).toEqual({ token: "ghs_cache_write_failed", installationId: 305, expiresAt: "2026-06-25T08:00:00Z" }); + expect(warn).toHaveBeenCalledWith(expect.stringContaining("orb_token_cache_write_failed")); + }); + + it("still returns a cached token when touching last_token_at fails", async () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-06-25T07:00:00Z")); + const e = await brokerEnv({ TOKEN_ENCRYPTION_SECRET: "orb-cache-test-secret" }); + await seedInstall(e, 306, { registered: 1 }); + const { secret } = (await issueOrbEnrollment(e, 306)) as { secret: string }; + const fetchCalls = countingTokenFetch("2026-06-25T08:00:00Z"); + + expect(await brokerOrbToken(e, secret)).toMatchObject({ token: "ghs_minted_1" }); + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + const originalPrepare = db(e).prepare.bind(db(e)); + vi.spyOn(db(e), "prepare").mockImplementation((sql: string) => { + if (sql.includes("SET last_token_at = CURRENT_TIMESTAMP")) { + throw new Error("timestamp write unavailable"); + } + return originalPrepare(sql); + }); + + expect(await brokerOrbToken(e, secret)).toEqual({ token: "ghs_minted_1", installationId: 306, expiresAt: "2026-06-25T08:00:00Z" }); + expect(fetchCalls()).toBe(1); + expect(warn).toHaveBeenCalledWith(expect.stringContaining("orb_token_last_touch_failed")); + }); + it("rejects an unknown or revoked enrollment", async () => { const e = await brokerEnv(); expect(await brokerOrbToken(e, "orbsec_bogus")).toEqual({ error: "invalid_enrollment" });