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
52 changes: 3 additions & 49 deletions src/orb/broker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,8 @@
// 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's 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 @@ -55,57 +50,16 @@ 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, cached_token_json FROM orb_enrollments WHERE secret_hash = ?")
.prepare("SELECT enroll_id, installation_id, state, revoked_at FROM orb_enrollments WHERE secret_hash = ?")
.bind(await hashToken(secret))
.first<{ enroll_id: string; installation_id: number; state: string; revoked_at: string | null; cached_token_json: string | null }>();
.first<{ enroll_id: string; installation_id: number; state: string; revoked_at: 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 throttles GitHub's token endpoint (16-20s responses → engine timeouts → orb_broker_unavailable).
// 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 cacheOrbToken(env, row.enroll_id, minted);
await touchLastToken(env, row.enroll_id);
await env.DB.prepare("UPDATE orb_enrollments SET last_token_at = CURRENT_TIMESTAMP WHERE enroll_id = ?").bind(row.enroll_id).run();
return { token: minted.token, installationId: row.installation_id, expiresAt: minted.expiresAt };
}

async function touchLastToken(env: Env, enrollId: string): Promise<void> {
await env.DB.prepare("UPDATE orb_enrollments SET last_token_at = CURRENT_TIMESTAMP WHERE enroll_id = ?").bind(enrollId).run();
}

/** 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) }));
}
}
59 changes: 0 additions & 59 deletions test/integration/orb-broker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,65 +68,6 @@ describe("brokerOrbToken", () => {
await db(e).prepare("UPDATE orb_github_installations SET suspended_at=CURRENT_TIMESTAMP WHERE installation_id=302").run();
expect(await brokerOrbToken(e, secret)).toEqual({ error: "installation_not_eligible" });
});

it("caches the minted token (encrypted) and serves it WITHOUT re-minting on the next exchange (#12)", async () => {
const e = await brokerEnv({ TOKEN_ENCRYPTION_SECRET: "test-encryption-key-material-0001" });
await seedInstall(e, 310, { registered: 1 });
const { secret } = (await issueOrbEnrollment(e, 310)) as { secret: string };
let mints = 0;
vi.stubGlobal("fetch", async () => {
mints += 1;
return Response.json({ token: `ghs_${mints}`, expires_at: new Date(Date.now() + 60 * 60_000).toISOString() });
});
const first = await brokerOrbToken(e, secret);
const second = await brokerOrbToken(e, secret);
expect(first).toMatchObject({ token: "ghs_1", installationId: 310 });
expect(second).toMatchObject({ token: "ghs_1" }); // served from the cache, NOT re-minted
expect(mints).toBe(1); // GitHub's token endpoint was hit ONCE across two exchanges (no throttling)
const cached = (await db(e).prepare("SELECT cached_token_json FROM orb_enrollments WHERE installation_id=310").first<{ cached_token_json: string }>())?.cached_token_json ?? "";
expect(cached).not.toContain("ghs_1"); // stored encrypted, never plaintext
expect(cached).toContain("ciphertext");
});

it("re-mints when the cached token is within the expiry margin (never serves a near-expired token)", async () => {
const e = await brokerEnv({ TOKEN_ENCRYPTION_SECRET: "test-encryption-key-material-0001" });
await seedInstall(e, 311, { registered: 1 });
const { secret } = (await issueOrbEnrollment(e, 311)) as { secret: string };
let mints = 0;
vi.stubGlobal("fetch", async () => {
mints += 1;
return Response.json({ token: `ghs_${mints}`, expires_at: new Date(Date.now() + 60 * 60_000).toISOString() });
});
// Seed a cache entry only ~5m from expiry (inside the 10m re-mint margin) — read returns before decrypting.
await db(e).prepare("UPDATE orb_enrollments SET cached_token_json = ? WHERE installation_id=311").bind(JSON.stringify({ ciphertext: "x", iv: "y", salt: null, expiresAt: new Date(Date.now() + 5 * 60_000).toISOString() })).run();
expect(await brokerOrbToken(e, secret)).toMatchObject({ token: "ghs_1" }); // a fresh mint, not the near-expired entry
expect(mints).toBe(1);
});

it("re-mints when the cached entry is unparseable (JSON/decrypt failure falls through)", async () => {
const e = await brokerEnv({ TOKEN_ENCRYPTION_SECRET: "test-encryption-key-material-0001" });
await seedInstall(e, 312, { registered: 1 });
const { secret } = (await issueOrbEnrollment(e, 312)) as { secret: string };
await db(e).prepare("UPDATE orb_enrollments SET cached_token_json = 'not-json' WHERE installation_id=312").run();
vi.stubGlobal("fetch", async () => Response.json({ token: "ghs_fresh", expires_at: new Date(Date.now() + 60 * 60_000).toISOString() }));
expect(await brokerOrbToken(e, secret)).toMatchObject({ token: "ghs_fresh" }); // malformed cache ignored, re-minted
});

it("swallows a cache-write failure — a valid token exchange never fails on a cache hiccup", async () => {
const e = await brokerEnv({ TOKEN_ENCRYPTION_SECRET: "test-encryption-key-material-0001" });
await seedInstall(e, 313, { registered: 1 });
const { secret } = (await issueOrbEnrollment(e, 313)) as { secret: string };
vi.stubGlobal("fetch", async () => Response.json({ token: "ghs_ok", expires_at: new Date(Date.now() + 60 * 60_000).toISOString() }));
const real = e.DB;
(e as { DB: unknown }).DB = {
prepare: (sql: string) =>
sql.includes("SET cached_token_json") ? { bind: () => ({ run: () => Promise.reject(new Error("cache write boom")) }) } : real.prepare(sql),
};
const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
expect(await brokerOrbToken(e, secret)).toMatchObject({ token: "ghs_ok" }); // mint succeeded despite the cache write failing
expect(warn.mock.calls.some(([l]) => String(l).includes("orb_token_cache_write_failed"))).toBe(true);
warn.mockRestore();
});
});

describe("broker endpoints", () => {
Expand Down