Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 53 additions & 3 deletions src/orb/broker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down Expand Up @@ -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<BrokerResult> {
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<void> {
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<void> {
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) }));
}
}
88 changes: 87 additions & 1 deletion test/integration/orb-broker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,19 @@ const seedInstall = (e: Env, id: number, cols: Record<string, string | number |
const brokerEnv = async (over: Partial<Env> = {}): Promise<Env> =>
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", () => {
Expand Down Expand Up @@ -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" });
Expand Down
Loading